尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

nginx proxy_set_header 详解:反向代理请求头配置与真实IP排查指南

nginx proxy_set_header 详解:反向代理请求头配置与真实IP排查指南 开头做后端接口和运维的同学应该都被proxy_set_header这几个词折磨过。明明 nginx 反代配好了后端也能通可接口一上线就出幺蛾子拿不到用户真实 IP、日志里全是 127.0.0.1、跳转链接变 http、Cookie 失效……翻来覆去查最后发现都是请求头没传对。说白了proxy_set_header解决的就是一个问题当 nginx 作为中间人转发请求时怎么把客户端原本的信息完整、准确地交到后端手里。这篇我把这个参数掰开揉碎讲清楚从原理到底层逻辑再到实际配置和常见坑适合刚接触 nginx 的初学者也适合配过但总出问题的老手对照排查。nginx 默认转发请求时会带上部分原始头但有些关乎业务判断的关键信息比如客户端 IP、原始协议、Host 域名并不会自动原样传递或者传过去的不是你想要的那个值。不理解这些配置就只能靠抄、靠试、靠猜。下面我一个一个拆讲清楚每个参数改的是什么、后端到底收到了什么以及为什么这么配。1. 为什么反向代理非要动这些头部参数1.1 后端服务器看到的假象先还原一个现场。假设你有一台 nginx监听 80 端口把所有/api/开头的请求转发给内网的 Spring Boot 服务。客户端浏览器访问的是http://example.com/api/user/infonginx 收到请求后把请求转发到http://192.168.1.10:8080/api/user/info。默认情况下不写任何proxy_set_header后端 Spring Boot 里通过request.getRemoteAddr()拿到的 IP 是什么不是用户的公网 IP而是 nginx 所在服务器的内网 IP。因为从 TCP 连接层面看和后端建立连接的确实就是 nginx后端根本感知不到客户端的存在。如果再仔细看后端收到的 HTTP 请求头Host可能也不是example.com而是192.168.1.10:8080。这会造成什么后果后端做域名校验时直接拒绝。后端生成重定向 URL 时用了错误的域名和端口。后端基于 IP 做限流、封禁全部打到 nginx 身上等于白配。日志分析根本分不清请求来自哪些真实用户。这不是 nginx 的 bug而是 HTTP 代理的天然特性。HTTP 请求本身只包含路径和头部Host头原本是给目标服务器判断虚拟主机用的Remote Address对端地址则是 TCP 层面的概念。代理介入后TCP 连接的终点变成了代理服务器原始信息自然就丢了。1.2 proxy_set_header 本质是手工补齐原始信息proxy_set_header就是在把请求转给上游upstream之前让你手动修改或新增 HTTP 请求头。它的作用是用 nginx 已经掌握的信息比如客户端的真实 IP$remote_addr、原始请求的Host、原始协议$scheme拼装成标准请求头塞进转发出去的请求里。从协议层面看这个指令就是修改出站请求的头部集合。但它有个很重要的特性需要注意proxy_set_header指令接收两个参数第一个是头名称第二个是值值里可以写固定字符串也可以引用 nginx 内置变量。这个值是在请求处理阶段动态解析的也就是说每次请求都不一样。另外还有一点容易忽略proxy_set_header可以在http、server、location三个层级使用子配置会整体覆盖父配置。很多同学在这里踩坑后面我会详细说。proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;这三行是出镜率最高的一组配置。但光知道要这么写不够后面每个变量还有门道。2. 核心参数逐个拆解名字、作用、取值逻辑2.1 Host后端路由和虚拟主机识别的关键Host头是 HTTP/1.1 里必须存在的请求头它的作用是告诉服务器你是在为哪个域名服务。nginx 在转发请求时如果不显式设置 Host会默认传递客户端原始请求的 Host 头。但是如果proxy_pass里写了完整 URI比如proxy_pass http://backend/;情况会不一样nginx 可能会用后端地址的主机名作为 Host。实际配置中常见的有两种写法proxy_set_header Host $host; proxy_set_header Host $http_host;区别很关键$host取请求行中的域名如果客户端请求里带了端口也不会带如果请求头里没有 Host会取 nginx 配置里server_name的值。取值顺序是请求行 Host → server_name。$http_host严格取客户端请求头里的Host字段原值带端口就带端口没带就没带。如果原始请求里没有这个头那这个变量就是空的。什么时候用哪个如果你的后端只按域名区分不带端口用$host比较干净如果后端要根据Host: 域名:端口做精确匹配比如某些网关或者虚拟主机路由用$http_host更准确。我遇到过一种情况nginx 配置了多个 server 块分别监听 80 和 443其中 443 是 HTTPS。前端页面通过 nginx 访问后端接口也是同一域名不同路径。这个时候如果后端需要用协议域名重定向单单传 Host 不够还要配合X-Forwarded-Proto才能让后端知道原始请求到底是 http 还是 https。这个后面单独讲。2.2 X-Real-IP 和 X-Forwarded-For真实客户端 IP 的传递链X-Real-IP是一个非标准的自定义头nginx 社区常用它来传递客户端真实 IP。常见配置是proxy_set_header X-Real-IP $remote_addr;$remote_addr是 nginx 内置变量表示与 nginx 建立 TCP 连接的客户端 IP。注意如果请求经过多层代理$remote_addr拿到的是上一层代理的 IP而不是最原始客户端的 IP。所以这行配置只适用于单层 nginx 代理的场景。X-Forwarded-For简称 XFF是真正意义上的标准扩展头用来记录整个代理链上每一跳的 IP。nginx 提供了一句话写好的变量proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;$proxy_add_x_forwarded_for的逻辑是取客户端请求里带的X-Forwarded-For值然后在末尾追加$remote_addr。举个例子客户端直连 nginx原始请求没有 XFF。nginx 变量值为客户端IP。客户端前面还有一层负载均衡比如云上的 LB已经加了 XFFnginx 收到请求时 XFF 是客户端IP。nginx 转发时$proxy_add_x_forwarded_for等于客户端IP, 上一层LB的IP也就是$remote_addr。理解这个追加逻辑很重要。在多层代理架构下后端拿到 XFF 后最左边的 IP 是最原始的客户端 IP最右边的 IP 是最后一跳代理的 IP。判断真实 IP 时通常取最左边第一个但前提是每一层代理都正确配置了追加逻辑且没有客户端伪造 XFF。这个安全话题后面章节展开。另外有一个容易犯的错有些同学直接写proxy_set_header X-Forwarded-For $remote_addr;这样会把原始 XFF 整个覆盖掉多级代理时后端只能看到上一层 IP。除非明确知道自己在做什么否则应该用$proxy_add_x_forwarded_for。2.3 X-Forwarded-Proto 和其他补充参数X-Forwarded-Proto用来告诉后端客户端和 nginx 之间建立连接时用的原始协议是什么。常见的配置proxy_set_header X-Forwarded-Proto $scheme;$scheme变量在 nginx 处理请求时会被解析为http或https。如果 nginx 通过 443 端口 SSL 终结后把请求转发给后端的 80 端口后端读取X-Forwarded-Proto就能判断原始协议是 https从而生成 https 的资源链接或者在判断安全 Cookie 时不会出问题。还有一个X-Forwarded-Host作用和 Host 类似但如果原始请求经过了多级代理每一级的 Host 可能被改写此时可以用它保留最初客户端的 Host。配置proxy_set_header X-Forwarded-Host $http_host;有些场景还会用到X-Forwarded-Port传原始端口常配合X-Forwarded-Proto一起用。可以写proxy_set_header X-Forwarded-Port $server_port;。X-Request-ID如果要追踪一个请求在整条链路上的轨迹可以在入口处生成一个唯一 ID通过头传给后端。nginx 本身没有内置这个变量需要借助$request_idnginx 1.11.0 之后内置每次请求生成一个 32 位十六进制随机字符串。示例proxy_set_header X-Request-ID $request_id;。这一节最后补一个表格方便查参数名常用取值后端收到后的典型用途Host$host或$http_host虚拟主机路由、域名校验X-Real-IP$remote_addr快速取客户端 IP单层代理X-Forwarded-For$proxy_add_x_forwarded_for获取完整代理链 IP 列表X-Forwarded-Proto$scheme判断原始协议是 http/httpsX-Forwarded-Host$http_host保留原始访问域名X-Forwarded-Port$server_port拿到原始访问端口X-Request-ID$request_id全链路日志追踪3. 实操一个完整的前后端分离项目配置3.1 环境准备与目标场景光讲参数太抽象我跑一个实际项目来演示。假设场景如下前端静态文件部署在/var/www/dist由 nginx 直接托管。后端Spring Boot 服务监听127.0.0.1:8080接口路径全部以/api/开头。域名example.com同时监听 80 和 443HTTPS 证书已配置。需求前端页面和 API 都通过同一个域名访问。后端需要拿到真实客户端 IP用来做操作日志记录。后端生成链接时必须识别原始协议避免把 https 页面里的接口地址重写成 http。后端有基于域名的校验逻辑要求 Host 保持为example.com不带端口。这个场景非常典型几乎每个前后端分离项目都会遇到。下面直接看 nginx 配置。3.2 配置清单与参数选择说明核心配置如下server { listen 80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/example.com.crt; ssl_certificate_key /etc/nginx/certs/example.com.key; root /var/www/dist; index index.html; # 静态资源直接由 nginx 处理不进入后端 location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Request-ID $request_id; proxy_connect_timeout 5s; proxy_read_timeout 30s; } }说明几个选择Host用的是$host而不是$http_host。因为这个场景里访问域名就是example.com没有特殊端口需求用$host可以保证后端收到的 Host 干净统一不会因为用户在地址栏输入example.com:443而带上端口。如果后端严格按端口区分这里就要改$http_host需要根据业务取舍。X-Real-IP和X-Forwarded-For都配了。X-Real-IP是给后端快速取单值用的X-Forwarded-For保留完整链路。两者不冲突建议同时配。X-Forwarded-Proto用$scheme。因为 nginx 这边 80 端口已经 301 跳到 443客户端实际上都是通过 HTTPS 访问到 nginx 的后端收到X-Forwarded-Proto: https就不会生成错误的 http 链接。proxy_http_version 1.1是个容易忽略但很重要的设置。默认情况下 nginx 向上游发请求用的是 HTTP/1.0而 HTTP/1.0 默认不启用 Keep-Alive也没有Host头的强约束。对于 WebSocket 和长连接场景必须改成 1.1。在普通 HTTP 接口场景下改成 1.1 也能让后端复用连接减少握手开销。3.3 验证结果后端到底收到了什么配置写完nginx -t检查语法然后nginx -s reload重载。接下来验证后端实际收到的请求头。最简单的验证方法在后端写一个临时接口把收到的所有 header 原样返回。我用 Python 起了一个临时服务来观察返回结果大致是这样{ headers: { Host: example.com, X-Real-IP: 61.144.xx.xx, X-Forwarded-For: 61.144.xx.xx, X-Forwarded-Proto: https, X-Forwarded-Port: 443, X-Request-ID: 78a2c3d4e5f60718293a4b5c6d7e8f90, Connection: keep-alive, User-Agent: Mozilla/5.0 ... } }注意看这几个点X-Real-IP和X-Forwarded-For此时都是同一个值因为客户端直连 nginx没有经过其他代理。如果有云负载均衡存在X-Forwarded-For会多出一个 IP。Host干净X-Forwarded-Proto正确识别了 https。后端基于这个组合可以做域名白名单校验也可以放心用request.getScheme()判断协议。如果发现后端拿到的 IP 是127.0.0.1先确认$remote_addr变量的实际值——那就是 nginx 认为的客户端。如果上层的负载均衡给 nginx 转发请求时没有正确传递原始 IPnginx 这边怎么配也救不回来需要从上一层解决。4. 高频问题与排查技巧实录4.1 拿到了内网 IP 或代理 IP而不是真实客户端 IP这是最常被问到的问题。现象后端日志里记录的用户 IP 全是10.x.x.x或者172.x.x.x之类内网地址看不到真实的外网 IP。排查思路从下往上捋先看 nginx 的默认access_log那个里面的 IP 是$remote_addr也就是 nginx 直连对端的 IP。如果 nginx 日志里已经是内网 IP说明给 nginx 转发请求的上一层设备负载均衡、云网关、另一台 nginx没有把原始 IP 传过来或者传的字段名不标准。如果 nginx 日志里是公网 IP但后端拿到的是内网 IP说明 nginx 配置里没有正确设置X-Real-IP或X-Forwarded-For。还有一个细节如果在云环境里云负载均衡默认会加一层X-Forwarded-For但有些产品并不保证X-Real-IP一定会传。这种情况下后端的读取逻辑要统一别一会儿取 X-Real-IP一会儿取 X-Forwarded-For 的第一个值。4.2 配置了多个 proxy_set_header但某些 header 没生效这个坑很多人踩过。proxy_set_header的作用域是整体覆盖不是单个头覆盖。看个例子location /api/ { proxy_pass http://backend; proxy_set_header Host $host; # 这里的 X-Forwarded-For 会失效 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /api/v2/ { proxy_pass http://backend_v2; proxy_set_header Host $host; # 注意这里没有设置 X-Forwarded-For }这个配置的问题在于/api/v2/这个 location 里只要出现了proxy_set_headernginx 就会认为这个上下文的 proxy header 集合就是我自己定义的这些不会再去继承父级或上一级的设置。所以 v2 接口转发时不会带X-Forwarded-For而后端可能就存在拿不到真实 IP 的问题。排查技巧与其逐段找配置不如在后端打一个临时接口把 header 全打出来一眼就能看出缺了什么。配置上的原则是如果多个 location 需要相同的 header建议提到 server 层让子级天然继承子级只需要补充自己的特殊情况。例如server { listen 443 ssl; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; location /api/ { proxy_pass http://backend; } location /ws/ { proxy_pass http://websocket_backend; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }这样 base 配置在 server 层统一管理子级不用重复也不容易漏。4.3 后端起服务不识别带下划线的业务头这个坑很隐蔽。很多人做微服务网关时想给后端传一个自定义头比如user_token或app_idnginx 配置写proxy_set_header user_token abc123;结果后端比如 Tomcat收到的请求里根本没有这个头。原因在于 nginx 在处理请求头时默认会丢弃包含下划线的头部字段这是出于安全考虑。如果确实需要传递带下划线的自定义头有两种解法改 nginx 的underscores_in_headers on;在 http 或 server 块中配置。更推荐的做法给自定义头命名时避开下划线改用连字符比如user-token、app-id。大多数后端的 Servlet 容器会自动把X-User-Token这样的头映射为xUserToken这类属性处理起来也规范。注意underscores_in_headers这个参数改的是 nginx接收客户端请求头时的行为如果你在proxy_set_header里设置带下划线头对 nginx 出站转发有影响吗实测中nginx 发出请求时不会主动丢弃带下划线头但为了保险和统一我还是建议避免下划线。4.4 WebSocket 代理场景下的特殊设置如果使用了 WebSocketproxy_set_header里必须额外处理两个头proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;原理是这样的WebSocket 握手时客户端会发Upgrade: websocket和Connection: Upgrade两个头nginx 默认在转发时不会保留它们因为 HTTP 头在代理之间传递时Connection头本来就用于逐跳hop-by-hop选项不能透传。如果不手动设置后端收到的就是一个普通 HTTP 请求握手直接失败。另外WebSocket 场景下连接通常要维持较长时间proxy_read_timeout建议调大否则默认 60 秒后连接会被 nginx 掐断。注意这里配置的Connection upgrade是一个固定字符串不要写成$connection_upgrade之类除非你在 map 里做了变量映射。一个小技巧可以在配置里用一个 map 统一处理 Connection 头兼容普通请求和 WebSocket 请求map $http_upgrade $connection_upgrade { default upgrade; close; } server { location /ws/ { proxy_pass http://ws_backend; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } }5. 一些安全与规范层面的经验提醒5.1 X-Forwarded-For 可以被伪造别直接拿来当认证凭据这是个很重要却被很多人忽视的点。客户端发请求时是可以自己随便写X-Forwarded-For头的。如果你在后端直接取 XFF 的第一个 IP 做黑名单、限流或者审计攻击者只需要在请求头里加一个X-Forwarded-For: 8.8.8.8就能把自己伪装成任意 IP。那$proxy_add_x_forwarded_for能防伪造吗不能完全防。它会以客户端传进来的 XFF 为基础追加 IP如果原始请求自带 XFF那它就会原样保留然后追加$remote_addr。所以后端要拿到可信的客户端 IP正确的做法是在 nginx 这一层用$remote_addr作为可信来源传给后端的头比如X-Real-IP应该是 nginx 自己生成的而不是拼接客户端传入的不可信 XFF。后端读取时如果信任 nginx比如只有 nginx 能访问后端可以优先使用X-Real-IP或者取 XFF 中最后一个由 nginx 追加的IP而不是第一个。不要把 XFF 作为唯一的安全校验依据它只能用于日志、展示、简单的网关识别。5.2 多级代理时的准确传递方案生产环境经常是多级代理客户端 → CDN/云负载均衡 → nginx → 后端。这种情况下每一级都必须正确传递和追加 XFF# 第一级入口负载均衡 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # 第二级nginx proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr;每一级拿到的$remote_addr都是上一级设备的 IP经过两级追加后XFF 看起来像客户端IP, 负载均衡IP, nginx内网IP后端取最后追加的那个 IP 就是最接近自己的代理 IP取中间是负载均衡 IP取最左边才是真实客户端 IP。但如同前面所说客户端可能伪造 XFF此时 XFF 里会有多个可疑 IP。严谨的做法是在入口处记录下真实客户端 IP$remote_addr用一个不对外暴露的自定义头传给后续服务比如X-Client-IP: $remote_addr后续所有服务以它为准。5.3 配置规范和经验提醒最后沉淀几条我在项目里一直坚持的规范都是用坑换来的统一在 server 层配置通用 headerlocation 层只做差异补充。这样不会漏配也方便后期排查。每次改完配置都要nginx -t再 reload。漏掉这一步上生产发现语法错误那就是事故。后端 Header 日志开局就打好上线前先确认真实 IP、协议、Host 是否符合预期别等用户反馈才排查。自定义 Header 用连字符不用下划线避免与 nginx 默认行为冲突。严格区分$host和$http_host涉及端口精确匹配时不要随便用。明白 XFF 不可信安全校验以$remote_addr为锚点。6. 从一次线上故障看代理头配置的价值我觉得有必要用一个真实的线上故障来收尾因为这类问题几乎每个公司的后端团队都会遇到而且排查过程非常典型。之前有个项目前端静态资源放在 CDN后端接口由 nginx 反代到内网服务。某天业务方反馈后台日志里所有用户的归属地都是同一个城市一看 IP 全是127.0.0.1。最开始怀疑是后端日志写错了翻了代码发现是取request.getRemoteAddr()。然后查 nginx 配置发现没有设置X-Real-IP和X-Forwarded-For后端拿到的是 nginx 本机回环地址。补上配置后日志能显示 CDN 节点的 IP但还不是最终用户 IP——因为 CDN 层也加了 XFF而 nginx 的$proxy_add_x_forwarded_for会把这个 XFF 带过去所以后端日志里看到的是 CDN 节点 IP 追加 nginx 内网 IP。后来我们在 CDN 配置里打开了透传真实 IP同时 nginx 也规规矩矩地用$proxy_add_x_forwarded_for追加不覆盖后端日志才终于显示正确的用户来源。整个排查过程说复杂也复杂说简单也简单任何一层代理配置少传或者错传了一个头真实 IP 链条就断了。这个故障让我养成了一个习惯任何涉及代理的服务第一件事就是把 IP 链路打通把协议、Host、追踪 ID 这些信息完整传到末端再去开发业务逻辑。日志、限流、审计、灰度全都依赖这条信息链。proxy_set_header看起来只是几个头的配置实际上决定了一个系统的可观测性和安全性。如果你现在的项目里还在为真实 IP 发愁或者不确定某个头该不该传建议直接在后端打个临时 echo 接口把 header 全部打出来配一轮看一轮很快就能摸清楚每条链路的行为。配完之后顺手写个自动检查脚本定期验证生产环境的头传递是否符合预期这个投入非常值。
返回列表