不知道你有没有遇到过这种场景:后端服务明明能收到请求,但日志里客户端的IP全是内网网段,或者服务端拿到的Host域名怎么都不对,再或者明明是HTTPS进来的,后端却认为是一次HTTP请求。十有八九,问题就出在Nginx反向代理那一层的proxy_set_header参数上。
很多刚入门的朋友配置反向代理,能写通proxy_pass就万事大吉,对proxy_set_header的态度是“抄别人配置、报错就删”,从没认真研究过这些请求头到底改变了什么。实际上,proxy_set_header解决的是反向代理场景下“真实客户端身份信息如何向后端传递”这个核心问题。无论是给后端透传真实IP、保持原始域名、还是处理HTTPS协议判定,都绕不开它。
这篇文章我不打算把官方文档复述一遍,而是结合我在生产环境里配置Nginx的经验,把proxy_set_header的常用参数、使用逻辑、真实场景配置,以及那些特别隐蔽的坑一次讲透。即使你之前完全没研究过这一块,照着我下面的思路也能把配置写明白。
1. 先搞明白proxy_set_header到底是干什么的
1.1 反向代理会让“身份信息”丢在哪一层
要理解proxy_set_header,得先搞清楚一次反向代理请求经历了什么。
假设用户在浏览器访问https://example.com,Nginx收到请求后,根据proxy_pass配置把请求转发给内网的一台Tomcat,Tomcat处理完再原路返回。这个过程中,Nginx实际上做了两件事:第一,接收客户端的TCP连接,此时Nginx知道客户端的IP(存在$remote_addr变量里);第二,作为HTTP客户端,向Tomcat发起一个新的HTTP请求。
问题就出在这第二次请求上。Nginx向Tomcat发起新请求时,默认情况下,HTTP请求头里的Host字段会被改成proxy_pass指定的上游地址,而不是用户浏览器里的example.com。同时,Tomcat看到的请求来源IP是Nginx的IP,而不是用户的真实IP。如果你不主动设置proxy_set_header,后端服务拿到的,就是一套“被代理加工过”的身份信息。
举个例子,我在一个PHP项目里见过这样的情况:应用统计用户地区,全部依赖$REMOTE_ADDR,结果部署到Nginx后面后,所有用户都显示成了服务器机房的位置。原因就是后端收到的REMOTE_ADDR是Nginx服务器的内网IP,根本没有拿到真实客户端IP。
1.2 不设置这些头,实际会发生什么
很多人觉得“不设置也能跑,为什么要折腾”。确实,简单的静态页面或接口转发不设置也可以,但一旦涉及下面这几类场景,不设置就会出问题:
- 虚拟主机路由错乱:后端同一台服务器上跑了多个域名,通过
Host头来区分站点。Nginx转发时如果默认把Host改成了上游IP,后端不知道你要访问哪个域名,很可能就把默认站点的内容返回给你。 - 用户IP统计失效:所有访问后端的IP都变成Nginx服务器的IP,日志分析、地域统计、频率限制全部失效。
- HTTPS协议判断错误:后端需要区分用户是通过HTTPS还是HTTP访问,用来生成绝对链接(比如支付回调地址)。如果Nginx没有传递原始协议信息,后端就会拿HTTP去生成链接,导致各种奇怪的回调问题。
- 安全策略误判:一部分安全组件会根据IP做黑白名单或风控,拿不到真实IP,这些策略全部形同虚设。
对proxy_set_header的理解,直接决定了你排查此类问题时的方向是否准确。它本质上是一组“身份信息透传指令”,告诉Nginx在转发请求时,如何构造或改写特定请求头。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频使用的参数逐个拆解:每个头都有它的脾气
2.1 Host参数:后端认定“你在访问谁”
Host请求头是HTTP/1.1协议里的标准头,后端服务常常用它来识别访问的是哪个域名、哪个虚拟主机。在Nginx里,和Host相关的值有几个容易混淆的变量:
| 变量 | 含义 | 示例 |
|---|---|---|
$host |
请求行中的域名,或Host头去除端口后的部分,不含端口,优先级为请求行 > Host头 > server_name |
example.com |
$http_host |
原始请求头里的Host内容,含端口,如果没有这个头则为空字符串 |
example.com:8080 |
$proxy_host |
proxy_pass中指定的上游主机名和端口 |
192.168.1.10:8080 |
最常见的配置是:
nginx复制proxy_set_header Host $host;
这样,后端收到的Host就是用户访问时用的域名(不含端口)。如果你的服务明确需要端口号,比如前端根据端口判断环境,可以改成$http_host。
那什么时候用$proxy_host?比如后端服务本身就绑定了一个内网域名,而且这个域名只在内网解析,Nginx的proxy_pass写的是http://internal-api.example.com,这种情况下如果你硬传$host,后端可能因为不认这个外网域名而拒绝服务。此时反而应该让Nginx用自己的上游主机名去访问,也就是不设置,或者显式设置proxy_set_header Host $proxy_host;。
2.2 X-Real-IP:最简单可靠的真实IP传递
X-Real-IP是一个非标准请求头,很多Nginx配置教程里都会让你加上这一行:
nginx复制proxy_set_header X-Real-IP $remote_addr;
它的意思是:在转发请求时,添加一个名为X-Real-IP的请求头,值为Nginx直连客户端的IP地址。$remote_addr在这里永远是建立TCP连接的那一端IP,如果是单层Nginx反向代理,它就是用户的真实IP。
这个头非常直接,适合在后端只需要一个简单真实IP的场景。对于Java、Python、PHP等后端来说,读取请求头里的X-Real-IP就能拿到客户端IP。
但要注意:X-Real-IP只是一个自定义头,并没有标准的覆盖逻辑。如果有多层代理,每一层都用这个字段时,一般约定是“覆盖为上一跳传来的真实IP”。所以在某些多层架构里,可能只在入口Nginx设置,后面的代理不再覆盖。
2.3 X-Forwarded-For:不能被覆盖,只能追加
X-Forwarded-For简称XFF,是业界处理代理链的标准方式。它和X-Real-IP最大的区别是:不覆盖,而是追加。
一个标准写法是:
nginx复制proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
$proxy_add_x_forwarded_for这个变量很有讲究。它等于“客户端请求头里原有的X-Forwarded-For值”加上“逗号分隔的$remote_addr”。也就是说,如果客户端请求本身带了XFF头,Nginx会在后面追加自己的直连IP;如果没带,就相当于直接写入当前直连IP。
举个多层代理的例子:
用户IP是1.2.3.4,先经过第一层Nginx(公网IP 100.100.100.100),再经过第二层Nginx(内网IP 192.168.1.10),后端收到XFF大致是:
code复制1.2.3.4, 100.100.100.100
第二层Nginx用$proxy_add_x_forwarded_for时,会把第一层传过来的链完整保留,再追加上自己的IP。这样后端就能分析出整条链路。
千万别写成:
nginx复制proxy_set_header X-Forwarded-For $remote_addr;
这样会把链里已有的用户IP直接覆盖掉,只剩当前Nginx看到的直连IP。在多层代理下,你的真实用户IP就丢了。
2.4 X-Forwarded-Proto和X-Forwarded-Host
这两个头是后端判断协议和域名的关键辅助信息,很多框架依赖它们生成跳转链接、安全cookie等。
nginx复制proxy_set_header X-Forwarded-Proto $scheme;
$scheme是Nginx和客户端之间实际使用的协议,取值为http或https。这样,即使Nginx内部用HTTP协议转发给后端,后端也能通过X-Forwarded-Proto知道用户原先是走HTTPS进来的。
nginx复制proxy_set_header X-Forwarded-Host $host;
X-Forwarded-Host用来传递用户原始访问的域名,和Host参数配合使用。在一些框架里,如果发现X-Forwarded-Host存在,会用这个值来生成站内绝对链接,避免出现“用户访问的是A域名,生成的链接却是内部B域名”这种事故。
3. 按场景抄配置:我常用的几套写法
3.1 最常见的Web应用反向代理
如果是给一个普通的Web应用做反向代理,后端是Tomcat、Node.js、Gunicorn这类服务,我一般会这样写:
nginx复制location / {
proxy_pass http://127.0.0.1:8080;
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;
}
这套配置足够通用。Host $host保证后端路由正确;X-Real-IP和X-Forwarded-For用于记录真实客户端IP;X-Forwarded-Proto用于区分HTTP/HTTPS。大多数后端框架,比如Django、Spring Boot、Express,都能通过这套请求头正确解析客户端真实信息。
3.2 HTTPS反向代理与协议透传
如果Nginx负责终结HTTPS,也就是SSL证书放在Nginx上,Nginx和后端之间走HTTP,那X-Forwarded-Proto就显得尤为重要。因为后端看到的TCP连接是来自Nginx的明文HTTP,如果它不知道原始协议是HTTPS,生成的绝对URL就全是http://,造成页面加载混合内容和回调地址错误。
除了X-Forwarded-Proto,还需要注意X-Forwarded-Port这个头,用于传递原始端口。有的后端既要区分协议,又要区分端口:
nginx复制proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
$server_port是Nginx接收到客户端请求时监听的那个端口。比如Nginx监听443端口,用户通过HTTPS访问,这个值就是443。后端拿到协议和端口后,才能拼出正确的绝对URL。
3.3 WebSocket长连接场景
WebSocket的握手过程依赖HTTP的Upgrade和Connection请求头。默认情况下,代理服务器为了维持自身的HTTP连接语义,可能会把Connection处理成close,导致WebSocket握手失败。
正确的配置是:
nginx复制map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://ws-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这里用map指令做了一层处理:如果客户端请求头里有Upgrade字段,就说明它是一个升级协议的请求(比如WebSocket),Connection就设置为upgrade;如果没有,就保持close。这样普通HTTP请求和WebSocket请求都能正确处理。
3.4 多域名复用同一后端
不少微服务架构里,多个域名会指向同一个Nginx,然后按域名规则转发到同一个后端服务,由后端根据Host头区分业务线。此时必须保证Host不被proxy_pass影响:
nginx复制server {
listen 80;
server_name a.example.com b.example.com;
location / {
proxy_pass http://backend_cluster;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
如果把这里写成proxy_set_header Host $proxy_host;,那后端所有请求的Host都变成backend_cluster对应的内网地址,它就没办法区分两个域名,业务隔离就失效了。
4. 生产环境里踩过的坑:这几类问题最隐蔽
4.1 proxy_set_header写错位置,导致整段配置不生效
proxy_set_header指令可以出现在http、server、location这三个层级。很多人把配置写在server块里,结果某个location里又单独写了一个proxy_set_header,这时location里的配置会整体覆盖server层的配置,而不是合并。
什么意思呢?如果你在server层写了:
nginx复制proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
然后在某个location里只写了:
nginx复制proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
那么这个location里实际生效的只有X-Forwarded-For,server层的Host和X-Real-IP在这个location里都不再生效。这是一个特别容易踩的坑,我在一次排查Session丢失问题时,查了半天发现是Host在某个接口的location里被覆盖没了。
所以我的习惯是:要么全部写在server层且不在location里重复设置;要么每个location写完整的一套,不要依赖继承。
4.2 多层代理时原始IP被链式覆盖丢失
我曾经接手过一个架构:用户流量经过CDN → 入口Nginx → 业务Nginx → 后端。前面的配置者为了图省事,每层Nginx都写了proxy_set_header X-Forwarded-For $http_x_forwarded_for;,结果后端拿到的一直是CDN节点的IP,用户真实IP完全找不到。
正确的链式写法是始终用$proxy_add_x_forwarded_for,它会自动把已有的XFF链保留并追加当前直连IP。千万不要用$http_x_forwarded_for去覆盖,那个变量只代表“客户端传来的XFF值”,并不包含当前这一跳的信息,会导致链条断裂。
4.3 X-Forwarded-For可以被伪造,别全盘信任
这一点是很多人忽略的安全问题。如果客户端直连Nginx,它可以随意在请求头里加上一个假的X-Forwarded-For,比如:
code复制X-Forwarded-For: 1.1.1.1
Nginx使用$proxy_add_x_forwarded_for时,会把客户端伪造的值也保留下来,最终后端拿到的XFF可能是:
code复制1.1.1.1, 真实IP
所以,如果后端在做频率限制、风控、IP白名单这类安全判断,不能直接取XFF的第一个值,否则就是给攻击者开了一扇门。正确做法是取XFF链里“由代理追加上去的最后一跳”,一般是从右往左找到第一个可信代理IP之前的那个地址。当然这属于另一个深层话题,但配置header时你必须知道这个风险。
4.4 日志格式里看不到真实IP,排查跑偏
proxy_set_header影响的是转发给后端的请求头,和Nginx自身的访问日志是两套逻辑。如果你希望Nginx的access.log里也记录真实客户端IP,需要在log_format里用对应的变量,比如:
nginx复制log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
在这里,$http_x_forwarded_for记录的是请求头里的XFF值。如果前面的CDN或代理没有传这个头,你看到的日志里这块就是空的。通过分析日志,可以反过来确认代理链路里的header是否透传完整。
5. 一份参数速查表,配置前先对照
5.1 常见场景的完整配置模板
我整理了几套在实际项目中验证过的配置,可以按需直接粘贴调整。
标准HTTP反向代理:
nginx复制location / {
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;
}
HTTPS终结代理:
nginx复制location / {
proxy_pass http://127.0.0.1:8080;
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;
}
WebSocket代理:
nginx复制map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location /ws/ {
proxy_pass http://ws-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
上游需要HTTPS转发:
如果Nginx自身要用HTTPS协议访问上游,并且想传递客户端证书或SNI,可以配置:
nginx复制location / {
proxy_pass https://upstream.example.com;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
这里Host用$proxy_host是因为上游可能要求SNI里的域名必须和证书匹配。
5.2 proxy_set_header支持哪些变量
在proxy_set_header的value里,可以使用Nginx的变量。除了前面提到的$host、$remote_addr、$scheme、$server_port,还有一些经常用到的:
| 变量 | 含义 | 典型用途 |
|---|---|---|
$http_user_agent(常写为$http_user_agent的变量形式) |
客户端原始User-Agent | 透传给后端做UA判断 |
$request_method |
请求方法(GET/POST等) | 自定义路由头 |
$uri |
当前请求的URI | 透传给后端做处理 |
$request_id |
Nginx生成的唯一请求ID | 全链路日志追踪 |
$http_cookie |
客户端Cookie原始值 | 透传Cookie |
$remote_user |
Basic Auth认证的用户名 | 透传认证信息 |
一个比较实用的组合是给请求加上一个唯一ID,方便前后端联调时定位日志:
nginx复制proxy_set_header X-Request-Id $request_id;
这样后端日志里记录的请求ID,和Nginx访问日志里的$request_id是一致的,排查问题非常方便。
5.3 我的几个调优习惯
配置proxy_set_header这么多年,我总结出几个习惯,供你参考。
第一,每一层代理都使用追加语义。不管架构几层,X-Forwarded-For统一用$proxy_add_x_forwarded_for,这样最不容易出错。
第二,明确区分内网和外网边界。内网多级代理之间转发的header可以精简,但入口Nginx到后端这一段,真实IP、协议、域名信息必须完整。如果链路里还有SLB、云负载均衡之类的组件,它们的透传行为要先摸清楚,有的云厂商默认就会追加一层XFF。
第三,变更后一定要看后端实际收到的header。怎么验证?可以临时在后端接口里把request.headers打印出来看一眼,或者在后端日志里输出关键header。不要只看Nginx配置是否写对了,要确认后端真的收到了。我在实践中发现,很多问题不是proxy_set_header配错了,而是中间某个环节把它吞掉了。
第四,涉及隐私合规的头别乱透传。比如Authorization这类敏感信息,如果不后端不需要,没必要通过proxy_set_header显式透传,减少泄面。Nginx默认会透传大部分原始请求头,显式设置反而让你对“到底传了什么”有更强的掌控力。
第五,别把代理当万能的安全边界。即使你正确传递了真实IP,也要清楚X-Forwarded-For是客户端可伪造的。真正要求严格的安全策略,应该在更靠近应用的层面对IP做二次校验。
如果你正在被后端拿不到真实IP、域名识别错误、HTTPS协议判断不准这类问题困扰,照着上面几个场景把proxy_set_header重新梳理一遍,大概率能找到原因。这个参数虽然看起来只是几行配置,但它决定了你的后端能否准确感知到用户的真实访问上下文,值得认真对待。
