Nginx反向代理配置到底怎么写
Nginx 反向代理看起来就是一行 proxy_pass,真正上线后却经常遇到 IP 丢失、协议判断错误、路径多一个斜杠和 WebSocket 断连。这篇从基础代理配置讲起,重点梳理请求头补充、原始 IP 获取、proxy_pass 路径规则和多层代理排查思路。
Nginx 反向代理,很多人的第一印象就是下面这一行:
proxy_pass http://127.0.0.1:8080;
配置能跑起来不难。
但一上线,问题就来了:后端看到的 IP 全是 127.0.0.1,应用判断请求不是 HTTPS,接口路径多了一段 /api,文件上传超时,WebSocket 还莫名其妙断开。
这些问题通常不是 Nginx 不能代理,而是我们只写了“转发地址”,没有把请求上下文一起传过去。
这篇不重复展开 Nginx 的 rewrite 语法,重点讲反向代理真正容易踩坑的几块:请求头怎么补、原始 IP 怎么拿、proxy_pass 的路径怎么拼,以及多层代理时到底谁才是真实客户端。
反向代理在转发什么
正向代理是客户端主动找代理服务器,代理服务器代替客户端访问外部资源。
反向代理则相反,客户端以为自己在访问业务服务,实际上先访问了 Nginx,Nginx 再把请求转给后端。
客户端
│
│ https://www.example.com/api/user
▼
Nginx 反向代理
│
│ http://127.0.0.1:8080/api/user
▼
应用服务
反向代理至少做了三件事:
- 接收客户端连接
- 按域名、路径或其他规则选择后端
- 把请求转发给后端,再把响应返回给客户端
实际项目里,Nginx 往往还顺便承担 HTTPS 终止、静态资源处理、负载均衡、限流和访问日志等工作。
这就带来一个关键变化:后端收到的请求,不再是客户端直接发来的原始请求,而是经过 Nginx 重新建立的一次请求。
如果不额外传递信息,后端自然只能看到 Nginx 自己的地址、协议和端口。
一份能用的基础配置
先看一份相对完整的基础配置:
upstream app_backend {
server 127.0.0.1:8080;
# server 127.0.0.1:8081;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
# 把原始请求的重要上下文传给后端
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_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
这份配置最重要的不是 upstream,而是下面几行 proxy_set_header。
它们是在告诉后端:虽然这次 TCP 连接是 Nginx 建立的,但最初的客户端、域名和协议是什么。
后面的原始 IP 和请求头问题,基本都围绕这几行展开。
请求头到底该补哪些
Host 不能丢
客户端访问的域名通常放在 Host 请求头里。
如果 Nginx 不做处理,转发到后端时 Host 可能变成 upstream 的名字,或者变成后端服务的地址。
proxy_set_header Host $host;
后端可能依赖 Host 做这些事:
- 多租户域名识别
- 生成绝对 URL
- 判断回调地址
- 路由到不同站点
- 校验允许的域名
$host 通常是经过 Nginx 规范化后的主机名。
如果业务确实需要带端口,可以考虑 $http_host,它更接近客户端传来的 Host 值,但也要注意端口和非法值问题。
X-Real-IP 传一个当前认定的客户端 IP
最常见的写法是:
proxy_set_header X-Real-IP $remote_addr;
在只有一层 Nginx、客户端直接连入的场景中,$remote_addr 通常就是客户端 IP。
后端可以读取 X-Real-IP,用于日志、风控、限流和审计。
但它并不是“永远可靠的原始 IP”。如果 Nginx 前面还有 CDN 或负载均衡,$remote_addr 可能是上一层代理的地址。
所以不要看到 X-Real-IP 就认为问题结束了,先把网络链路画清楚。
X-Forwarded-For 保留代理链
X-Forwarded-For 通常用来记录请求经过的 IP 链:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
假设客户端 IP 是 203.0.113.10,请求先经过 CDN,再经过 Nginx,最终可能形成类似这样的值:
X-Forwarded-For: 203.0.113.10, 10.10.1.20
左边通常是更早加入链路的客户端地址,右边是后续代理追加的地址。
这里一定要用 $proxy_add_x_forwarded_for,不要无脑写成:
proxy_set_header X-Forwarded-For $remote_addr;
后者会直接覆盖上游已经传来的 X-Forwarded-For,导致代理链丢失。
X-Forwarded-Proto 告诉后端原始协议
HTTPS 在 Nginx 处终止后,Nginx 到后端可能走的是普通 HTTP。
如果不传原始协议,后端看到的就是 HTTP。
proxy_set_header X-Forwarded-Proto $scheme;
这样后端就知道客户端最初是通过 HTTP 还是 HTTPS 访问的。
这个头经常影响:
- 登录 Cookie 是否设置
Secure - 重定向地址使用 http 还是 https
- 应用生成的绝对链接
- Spring、Django、Laravel 等框架的安全判断
常见的“HTTPS 页面跳 HTTP”“登录后无限重定向”,就可能是这里没有传对。
一组常见请求头配置
实际项目里通常会一起写:
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-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
不是每个应用都需要全部字段,但统一传递这些上下文,后端排查问题会省很多事。
怎么获取真实客户端 IP
这块最容易被一句“取 X-Forwarded-For 第一个 IP”带偏。
真正的关键不是哪个头看起来像真实 IP,而是:哪些代理是可信的。
只有一层代理
如果客户端直接访问 Nginx,可以这样配置:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
应用读取 Nginx 转发的 X-Real-IP 即可。
但应用不要直接无条件信任客户端自己提交的 X-Real-IP。
客户端完全可以自己构造:
X-Real-IP: 127.0.0.1
如果入口没有覆盖这个头,后端拿它做白名单判断就会有安全问题。
前面还有负载均衡
如果链路是这样:
客户端 -> CDN/负载均衡 -> Nginx -> 应用
那么 Nginx 看到的 $remote_addr 可能是 CDN 或负载均衡的 IP。
这时要使用 Nginx 的 realip 模块,并且只信任明确的代理网段:
# 这里只是示例网段,生产环境要替换成真实可信代理地址
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
配置后,Nginx 会在可信代理传来的 X-Forwarded-For 链中继续寻找客户端地址,并更新 $remote_addr。
然后再转发:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
set_real_ip_from 非常重要。
不要为了“获取真实 IP”把它配置成全网信任,例如 0.0.0.0/0。
那样任何客户端都能伪造代理链,日志、限流和权限判断都会被带偏。
后端到底信哪个 IP
建议在系统里统一约定,不要每个服务各写一套:
- 只有来自可信 Nginx 网段的请求,才允许读取代理头
- 由边缘层负责清理或覆盖客户端伪造的代理头
- 明确使用
X-Real-IP还是X-Forwarded-For - 多级代理场景记录完整链路,便于审计和排错
IP 获取是网络信任问题,不只是字符串解析问题。
proxy_pass 路径怎么改
proxy_pass 最容易踩坑的地方,就是末尾有没有 /。
假设配置是:
location /api/ {
proxy_pass http://app_backend;
}
客户端请求:
/api/user/list
由于 proxy_pass 没有 URI 部分,Nginx 通常会把原始请求 URI 传给后端:
后端收到:/api/user/list
如果改成:
location /api/ {
proxy_pass http://app_backend/;
}
这时匹配到的 /api/ 会被替换成 /,后端收到的路径通常是:
后端收到:/user/list
可以记成这张表:
| 配置 | 请求路径 | 后端路径 |
|---|---|---|
location /api/ + proxy_pass http://backend |
/api/user |
/api/user |
location /api/ + proxy_pass http://backend/ |
/api/user |
/user |
location / + proxy_pass http://backend |
/user |
/user |
location / + proxy_pass http://backend/ |
/user |
/user |
最后一行看起来一样,是因为 location 本身就是根路径,不容易看出差异。
真正容易出问题的是 /api/、/gateway/ 这种带前缀的 location。
用 rewrite 明确改路径
如果路径规则复杂,不要只靠尾部斜杠猜。
可以用 rewrite 把意图写清楚:
location /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://app_backend;
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;
}
这里的 break 表示改写 URI 后继续在当前 location 中处理,不重新走一轮 location 匹配。
rewrite 和 proxy_pass 不是一回事:
rewrite负责改 URIproxy_pass负责把请求转给上游return 301或return 302才是给客户端发重定向
不要把内部改写和外部重定向混在一起。
末尾斜杠也要统一
下面两个请求并不一定会命中同样的规则:
/api/user
/api/user/
如果业务路径有严格约定,建议通过 location 和重定向规则统一尾斜杠,而不是让后端每个接口自己猜。
路径问题排查时,最有效的办法是把三件事都打印出来:客户端请求 URI、Nginx access log 中的 URI、后端实际收到的 URI。
不要只看浏览器地址栏。
WebSocket 和长连接
普通 HTTP 代理通了,不代表 WebSocket 也能通。
WebSocket 需要通过 Upgrade 头把连接从 HTTP 升级为长连接,Nginx 要显式传递相关头:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location /ws/ {
proxy_pass http://app_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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
这里的 proxy_http_version 1.1、Upgrade 和 Connection 通常缺一不可。
长连接还要单独看 proxy_read_timeout,默认超时时间不适合长时间没有数据返回的连接。
但超时也不能无脑改成无限大,否则连接泄漏时会长期占着资源。
日志和超时别漏掉
反向代理出问题时,日志比“我感觉配置没问题”可靠得多。
建议至少关注这些信息:
- 客户端 IP 和代理链
- Host 和请求 URI
- 上游地址
- 上游响应状态码
- 上游响应耗时
- 请求总耗时
- 请求体大小和响应体大小
可以自定义 access log,方便把代理前后的信息串起来:
log_format proxy_main '$remote_addr - $host [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'upstream=$upstream_addr';
access_log /var/log/nginx/access.log proxy_main;
配置文件中包含双引号时,写入 JSON 或脚本配置文件要注意转义;直接在 Nginx 配置文件里则按 Nginx 语法书写即可。
常见超时可以这样理解:
proxy_connect_timeout:Nginx 连接后端要等多久proxy_send_timeout:向后端发送请求的写超时proxy_read_timeout:等待后端响应数据的读超时
接口慢不一定是 Nginx 的错,先看 upstream_response_time,确认时间到底耗在连接、Nginx 还是应用。
一套排查顺序
遇到“代理访问异常”,我一般按这个顺序排:
- 先确认 Nginx 配置语法:
nginx -t - 在 Nginx 机器上直接 curl 后端地址,确认应用本身可访问
- 检查
proxy_pass的尾部斜杠和实际转发路径 - 查看后端收到的 Host、协议、IP 和 URI
- 检查
X-Forwarded-For是否被覆盖或伪造 - 如果是 HTTPS,确认
X-Forwarded-Proto是否为https - 如果是 WebSocket,确认 Upgrade 头和 HTTP/1.1
- 最后再看超时、上传大小和缓冲区配置
可以先用 curl 构造请求头验证:
curl -i https://example.com/api/user \
-H 'X-Debug-Request: mebugs'
然后在后端日志里确认请求有没有按预期到达。
不要一上来就改十几个配置项,改完发现好了,也不知道到底是哪一项起作用。
小结
Nginx 反向代理的核心不是把请求“转过去”这么简单,而是要把请求的上下文传完整。
Host告诉后端客户端访问了哪个域名X-Real-IP传递当前认定的客户端 IPX-Forwarded-For保留多级代理链X-Forwarded-Proto告诉后端原始请求是 HTTP 还是 HTTPSproxy_pass末尾斜杠决定路径前缀是否被替换rewrite改的是 URI,proxy_pass做的是上游转发- WebSocket 需要 Upgrade、Connection 和 HTTP/1.1
- 获取真实 IP 前,先建立可信代理边界
一句话总结:反向代理配置的难点,不在于能不能转发,而在于转发之后后端还能不能正确理解原始请求。
配置改完记得 nginx -t,再用真实请求验证 Host、IP、协议和路径。配置文件看着正确不算完,后端实际收到什么才是最终答案。
