有段时间帮一个电商项目排查后端日志,发现所有订单都记成了同一个客户端IP,而用户分布在五湖四海。一开始后端同事怀疑代码问题,后来抓包一看,问题出在Nginx反代那一层:请求头里压根没有真实IP信息,后端拿到的全是Nginx本机内网地址。
后来我在Nginx的location里补上几行proxy_set_header,把Host、X-Real-IP、X-Forwarded-For、X-Forwarded-Proto这些头都处理好,问题立刻消失。
如果你也经常跟Nginx反向代理打交道,会发现proxy_set_header是绕不开的参数。它不复杂,但坑特别多:设置错了,轻则后端拿不到真实IP,重则首页302重定向到内网地址、登录态丢失、HTTPS链接被改写。这篇文章就把这个参数彻底拆开,讲清楚原理、常用配置和排查思路,希望能帮你少踩一些我踩过的坑。
1. proxy_set_header到底是什么:先搞懂它管的是“出口请求头”
1.1 Nginx把请求转给上游时,头信息并不会自动“全量透传”
很多人以为Nginx做反向代理,就是把客户端请求原封不动转给后端,所以后端自然能看到所有请求头。这个想法只对了一半。
Nginx转发请求前,会重新构造一份发给上游的HTTP请求。哪些头保留、哪些头改掉、哪些头新增,由proxy_set_header指令控制。如果不显式配置,Nginx会使用模块里的默认行为:比如把Host设成$proxy_host(也就是proxy_pass后面写的上游地址),把Connection设成close,而像X-Real-IP、X-Forwarded-For这类头,默认根本没有,自然也不会凭空出现在上游请求里。
所以,后端能不能拿到用户真实IP,取决于作为反向代理的Nginx有没有用proxy_set_header把信息“塞”进转发请求。这个指令管的是Nginx发给上游的请求头,不是客户端发给Nginx的请求头,也不是Nginx返回给客户端的响应头。想明白这一点,很多配置混乱就不会出现。
我见过有人把proxy_set_header写进了if块里,也见过有人把它写在location外面想让整个server都生效,结果被其他location意外覆盖,排查半天。核心原则是:先明确这个指令只有http、server、location三个层级能写,而且以下层配置为准,同级里同名头后者覆盖前者。
1.2 指令的执行层级与同名头覆盖规则
proxy_set_header可以放在http块、server块和location块。越具体的位置优先级越高:
- location里配置了某组头,会覆盖server块同名的配置;
- server块里配置了,会覆盖http块里的同配置;
- 同一层级内如果重复设置同名Header,后面的覆盖前面的,不会发两个相同的头给后端。
实际工作中,我建议把“所有反代请求都需要”的通用头放在location级别或server级别。
如果一个Nginx同时跑静态文件、PHP和反向代理,不要把proxy_set_header全局乱写。比如静态文件请求根本不需要X-Forwarded-Proto,但你写上去了也不影响返回内容,只是显得不够干净。更好的是在反代专用location中集中配置,这样别人看配置也直观。
常用的组合长这样:
nginx复制server {
listen 80;
server_name example.com;
location /app/ {
proxy_pass http://backend_server;
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_pass后面的地址可以是upstream名,也可以是具体IP端口。只要进到这个location转发,Nginx就会在出口请求上覆盖这四个头。下面逐个展开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频必配的4个头:Host、X-Real-IP、X-Forwarded-For与协议头
2.1 Host头:别让后端拿到内网IP当域名
Host是HTTP/1.1里很重要的头,很多后端程序依靠它来做域名路由、拼接URL、生成重定向地址。Nginx默认会把Host设成$proxy_host,即proxy_pass里写的上游地址,比如10.0.0.2:8080。后端程序如果按这个值去拼跳转链接,用户就会看到一个类似http://10.0.0.2:8080/login的内网地址,直接访问自然失败。
如果你希望后端收到的域名和用户在浏览器地址栏里输入的一致,常见做法是设成$host或$http_host。两者区别:
$host:取请求行中的域名,或请求头Host,或server_name,统一转小写,去掉端口。如果你的nginx监听80端口对外服务,用它就够了。$http_host:原样取自客户端请求里的Host头,会保留端口号和原始大小写。如果外部通过非标准端口访问,比如https://example.com:8443,需要保留端口时用$http_host。
我自己的偏好是:对外
