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

建议在系统里统一约定,不要每个服务各写一套:

  1. 只有来自可信 Nginx 网段的请求,才允许读取代理头
  2. 由边缘层负责清理或覆盖客户端伪造的代理头
  3. 明确使用 X-Real-IP 还是 X-Forwarded-For
  4. 多级代理场景记录完整链路,便于审计和排错

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 负责改 URI
  • proxy_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 还是应用。

一套排查顺序

遇到“代理访问异常”,我一般按这个顺序排:

  1. 先确认 Nginx 配置语法:nginx -t
  2. 在 Nginx 机器上直接 curl 后端地址,确认应用本身可访问
  3. 检查 proxy_pass 的尾部斜杠和实际转发路径
  4. 查看后端收到的 Host、协议、IP 和 URI
  5. 检查 X-Forwarded-For 是否被覆盖或伪造
  6. 如果是 HTTPS,确认 X-Forwarded-Proto 是否为 https
  7. 如果是 WebSocket,确认 Upgrade 头和 HTTP/1.1
  8. 最后再看超时、上传大小和缓冲区配置

可以先用 curl 构造请求头验证:

curl -i https://example.com/api/user \
  -H 'X-Debug-Request: mebugs'
​

然后在后端日志里确认请求有没有按预期到达。

不要一上来就改十几个配置项,改完发现好了,也不知道到底是哪一项起作用。

小结

Nginx 反向代理的核心不是把请求“转过去”这么简单,而是要把请求的上下文传完整。

  • Host 告诉后端客户端访问了哪个域名
  • X-Real-IP 传递当前认定的客户端 IP
  • X-Forwarded-For 保留多级代理链
  • X-Forwarded-Proto 告诉后端原始请求是 HTTP 还是 HTTPS
  • proxy_pass 末尾斜杠决定路径前缀是否被替换
  • rewrite 改的是 URI,proxy_pass 做的是上游转发
  • WebSocket 需要 Upgrade、Connection 和 HTTP/1.1
  • 获取真实 IP 前,先建立可信代理边界

一句话总结:反向代理配置的难点,不在于能不能转发,而在于转发之后后端还能不能正确理解原始请求。

配置改完记得 nginx -t,再用真实请求验证 Host、IP、协议和路径。配置文件看着正确不算完,后端实际收到什么才是最终答案。

更多推荐

章节目录