Nginx 反向代理实战指南:proxy_pass 末尾斜杠、proxy_redirect、sub_filter、WebSocket 与 upstream 全场景配置
Nginx 反向代理的核心翻车点是 proxy_pass 末尾斜杠的语义、proxy_redirect 改写后端 Location 头、sub_filter 替换响应体绝对路径。本文按整站、子目录、泛目录、WebSocket、流式、上传大文件、HTTPS 终结等十类场景给出最小可工作配置,并补全 upstream 五种调度算法、proxy_cache 缓存、cookie 域名改写等生产细节。
Nginx 反向代理是把内部服务藏在统一域名背后的核心手段,但实际配置里至少有十类常见场景:泛目录、子目录、整站、带斜杠与不带斜杠的 proxy_pass 行为差异、proxy_redirect 修正 location 头、sub_filter 替换响应体绝对路径、WebSocket 升级、流式输出、上传大文件、grpc 转发。本文按场景给出最小可工作的 nginx server 块配置,并讲清每条指令的边界条件——比如 proxy_pass 末尾斜杠的有无会影响整个请求路径的拼接逻辑,sub_filter 只对未压缩响应体生效等等。
反向代理与正向代理的边界
先把概念厘清:正向代理面向客户端,客户端主动配置 proxy server 让它代为请求外网(公司内网用 squid 出网就是典型);反向代理面向服务器,客户端不知道实际服务在哪台机器,nginx 在前面伪装成统一入口(CDN、WAF、网关都是反向代理)。本文涉及的全部是反向代理。
反向代理的核心价值是:
- 统一入口:客户端只见到 example.com,背后可能是 Java 微服务、Python 服务、Go 网关多个进程并存。
- SSL 卸载:HTTPS 在 nginx 终结,后端走 HTTP 减少握手开销。
- 负载均衡:upstream 块加多台后端做轮询、加权、ip_hash。
- 缓存:proxy_cache 把热点内容缓存在 nginx 层。
- 请求改写:rewrite + proxy_pass 把 URL 形态映射到后端期望的形态。
proxy_pass 末尾斜杠的关键差异
这是 nginx 反向代理最容易翻车的地方,没有之一。
规则简述
当 proxy_pass 后的 URL 带 URI(包括末尾斜杠 /),nginx 会把 location 匹配的部分从请求 URI 中替换掉;不带 URI 时,原始 URI 完整传递到后端。
四种组合的实际行为
假设 location 是 /abc/,请求 /abc/foo/bar?x=1,对应四种 proxy_pass 写法的转发结果:
| proxy_pass 写法 | 转发到后端的 URI |
|---|---|
| proxy_pass http://backend; | /abc/foo/bar?x=1(完整传递) |
| proxy_pass http://backend/; | /foo/bar?x=1(去掉 /abc) |
| proxy_pass http://backend/api; | /api/foo/bar?x=1(替换 /abc 为 /api) |
| proxy_pass http://backend/api/; | /api/foo/bar?x=1(同上) |
注意:第三、第四种结果一样,但实现机制不同。第三种是 nginx 把 /abc/ 替换成 /api,然后拼上 /foo/bar,结果是 /api/foo/bar。第四种 nginx 把 /abc 替换成 /api/,再拼上 foo/bar,结果也是 /api/foo/bar。一致是因为运气好。
规则失效的特殊场景
下面三种情况 proxy_pass 不能带 URI,nginx 会启动报错:
- location 用了正则匹配(
location ~ ^/api/) - location 内部用了 rewrite ... break
- proxy_pass 后是变量(
proxy_pass $upstream;)
这些场景下 nginx 没法做"替换 URI"动作,只能完整传递。如果你必须改写 URI,要在 location 里用 rewrite + proxy_pass 不带 URI 组合:
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /v2/$1 break;
proxy_pass http://backend;
}三种基础场景的最小配置
整站反向代理
典型用途:example.com 反代到内部某台 IP 上的整站。
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://192.168.1.10: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 是生产标配:
- Host:让后端拿到原始请求 Host,不然看到的是 nginx 的 IP。
- X-Real-IP:客户端真实 IP,用于日志、风控、地域识别。
- X-Forwarded-For:经过的代理链,多级代理时按逗号拼接。
- X-Forwarded-Proto:让后端知道客户端是 HTTPS 还是 HTTP,防止后端生成的 URL 协议错。
子目录反向代理(带前缀)
典型用途:example.com/api/* 反代到独立 API 服务,example.com/* 走前端。
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://192.168.1.20:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
proxy_pass http://192.168.1.10:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}注意 location /api/ 的 proxy_pass 末尾必须带 / —— 让 nginx 把 /api 前缀去掉再转发。如果不带 /,后端会收到 /api/users 而不是 /users,绝大多数后端会 404。
泛目录(不带末尾斜杠的 location)
典型用途:example.com/abc 与 example.com/abc/foo 都转发到某后端。
server {
listen 80;
server_name example.com;
location /abc {
proxy_pass http://192.168.1.30/abc;
}
}这种写法最大的坑是 example.com/abcd 也会命中 location /abc——nginx 的 location 前缀匹配不要求末尾斜杠对齐。如果要严格匹配 /abc/* 而不命中 /abcd,必须写 location /abc/。
proxy_redirect 处理后端发的 301/302
问题描述
后端服务返回 302 跳转时,Location 头里带的是后端自己的 URL(比如 http://backend/login)。浏览器收到这个 Location 后会直接访问 backend,绕过 nginx。如果 backend 在内网,浏览器跳到内网 IP 立刻失败;如果 backend 在公网,跳过 nginx 等于绕过了 SSL 终结、WAF、统计。
修复方案
用 proxy_redirect 把后端发的 Location 头改写成 nginx 路径:
location /my/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_redirect http://backend/ http://$host/my/;
}这条 proxy_redirect 把 Location: http://backend/login 改写成 Location: http://example.com/my/login。三种语法形式:
proxy_redirect default;—— 默认行为,nginx 自动尝试做合理替换,对简单场景够用。proxy_redirect off;—— 完全不改写。proxy_redirect http://backend/ http://$host/my/;—— 显式指定改写规则。
HTTPS 终结场景的特别注意
如果 nginx 终结 HTTPS、后端是 HTTP,proxy_redirect 必须显式写:
proxy_redirect http://backend/ https://$host/my/;否则后端发的 http:// Location 头会让浏览器跳到 http 站,触发 mixed content 警告或跳转。
相对路径的 Location 头
有些后端返回的 Location 是相对路径(Location: /login),这种情况 proxy_redirect 默认会处理:把 / 替换为 /my/。如果你显式写了规则但没覆盖相对路径,需要再加一行:
proxy_redirect / /my/;响应体内绝对路径的替换:sub_filter
问题:写死的 /public、/static、/api
很多老式 web 应用在模板里写死 <script src="/static/app.js">,反代到 example.com/my/ 时浏览器会请求 example.com/static/app.js 而不是 example.com/my/static/app.js。如果 example.com 根目录另有 /static/ 路径属于其它服务,请求就会落到错误的地方。
sub_filter 模块基础
nginx 编译时需要带 --with-http_sub_module,主流 Linux 发行版的 nginx-extras 包都带这个模块。检查方法:nginx -V 2>&1 | grep -o sub_module。
location /my/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_redirect / /my/;
sub_filter 'href="/' 'href="/my/';
sub_filter 'src="/' 'src="/my/';
sub_filter 'action="/' 'action="/my/';
sub_filter_types text/html text/css application/javascript;
sub_filter_once off;
}三个关键限制
使用 sub_filter 之前要清楚它做不到什么:
- 不能处理压缩响应。如果后端发的是 gzip 压缩的响应体,nginx 看到的是字节流不是文本,sub_filter 默默失效。修复:在 location 里加
proxy_set_header Accept-Encoding "";强制后端返回未压缩响应。代价是 nginx 出口流量变大,可以让 nginx 自己重新 gzip。 - 只能处理静态生成的内容。JS 框架(React、Vue、Angular)在浏览器端动态拼出的 URL 不在响应体里,sub_filter 完全够不着。这种情况需要改前端代码(用 webpack publicPath)或者在前端打包时通过环境变量注入前缀。
- 多个 sub_filter 的限制。nginx 1.9.4 之前一个 location 内只允许一个 sub_filter。1.9.4 之后允许多个,但仍按顺序执行,前一条改变后的内容不会触发后一条匹配。
WebSocket 反向代理
WebSocket 需要 HTTP/1.1 与 Upgrade、Connection 头才能升级协议。默认配置下 nginx 把这两个头剥掉了,导致 WebSocket 无法建立。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ws.example.com;
location / {
proxy_pass http://websocket_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_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}四个关键点:
- proxy_http_version 1.1:WebSocket 必须 HTTP/1.1,nginx 默认对后端用 1.0。
- map 块定义 $connection_upgrade:WebSocket 升级请求 Connection 头是 upgrade,普通请求是 close,map 优雅处理两种情况。
- proxy_read_timeout/send_timeout:默认 60s,长连接 WebSocket 一闲置就被切。改成 3600s(1 小时)或更长。
- 不要 proxy_buffering:WebSocket 是流式,缓冲会导致消息延迟。建议加
proxy_buffering off;。
流式响应(SSE / 长轮询)
Server-Sent Events、GPT 类 streaming API、curl --raw 实时输出等场景,nginx 默认会把响应整段缓冲后再发给客户端,破坏流式效果。
location /stream/ {
proxy_pass http://backend/;
proxy_buffering off;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection '';
chunked_transfer_encoding on;
proxy_read_timeout 24h;
}关键四项:proxy_buffering off 关闭响应缓冲;proxy_request_buffering off 让客户端慢上传也立刻流向后端;Connection 设空字符串避免 Keep-Alive 头被改;chunked_transfer_encoding on 让 nginx 用 chunked 编码发送给客户端。
上传大文件的代理
默认 nginx 限制 client_max_body_size 1MB,上传大文件直接 413。完整调优:
http {
client_max_body_size 500m;
client_body_buffer_size 128k;
client_body_timeout 600s;
}
server {
location /upload/ {
proxy_pass http://backend/;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
}proxy_request_buffering off 让 nginx 边收边转,不在 nginx 本地缓冲整个请求体,对大文件上传场景可以省掉 nginx 服务器磁盘 IO。
负载均衡的 upstream 配置
upstream backend {
least_conn;
server 192.168.1.10:8080 weight=3 max_fails=3 fail_timeout=30s;
server 192.168.1.11:8080 weight=2 max_fails=3 fail_timeout=30s;
server 192.168.1.12:8080 backup;
keepalive 32;
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}upstream 五种调度算法
- round_robin:默认轮询,按 weight 分配。
- least_conn:发送给当前连接数最少的后端,对连接耗时差异大的场景(流式、WebSocket)效果好。
- ip_hash:按客户端 IP 计算 hash 路由,同一 IP 总是去同一台后端。session sticky 的简易实现。
- hash $request_uri consistent:按请求 URI 一致性 hash,缓存场景常用。
- random two least_conn:随机选两台再用 least_conn 选其一,配合分布式系统的二选一最优策略。
keepalive 的关键性
upstream 块内的 keepalive 32; 让 nginx 与后端保持 32 个长连接复用,避免每次请求都新建 TCP。配合 server 块内的 proxy_http_version 1.1; proxy_set_header Connection ""; 才生效。我曾遇过没设 keepalive 的高并发站点,nginx 与后端之间每秒新建几千个 TCP 连接,把内核 nf_conntrack 表撑爆。
缓存:proxy_cache 减少回源
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:100m max_size=10g inactive=60m use_temp_path=off;
server {
location / {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 304 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
}
}关键点:proxy_cache_key 默认包含 host,多域名共用同一缓存目录时这个 key 防止串数据;proxy_cache_use_stale 让后端崩了时仍然返回过期缓存兜底;proxy_cache_lock 防止缓存击穿(同一 key 多个并发请求只放一个回源);X-Cache-Status 头返回 HIT/MISS/BYPASS/STALE 让你能在浏览器观察。
HTTPS 反向代理的特殊配置
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
location / {
proxy_pass http://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 https;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}常见错误是 X-Forwarded-Proto 没显式写 https,让后端误以为是 http 请求并生成 http 链接,浏览器加载时被 mixed content 拦截。
实战调试技巧
看真实转发出去的请求
临时在 location 里加 error_log /var/log/nginx/debug.log debug;,再 reload,能看到 proxy_pass 实际拼出的 URL、各 header 的值。debug 日志非常详细,调好后立刻关掉避免磁盘塞满。
tcpdump 抓 nginx 与后端通讯
tcpdump -i any -A -s 0 'host 192.168.1.10 and port 8080'能看到 nginx 转发的完整请求体与后端响应。比看日志更精确,特别是定位 Header 是否正确传递。
curl 模拟客户端
curl -v -H "Host: example.com" http://nginx_ip/api/test-v 看完整请求/响应,-H 强制设置 Host 让 nginx 路由到对应 server 块。
常见故障
故障 1:404 Not Found
多半是 proxy_pass 末尾斜杠错了。重新对照本文"四种组合"表,确认转发到后端的 URI 与后端期望的一致。
故障 2:upstream timed out (110)
nginx 与后端之间的 TCP 连接断了或者后端响应慢于 proxy_read_timeout(默认 60s)。看后端日志确认是后端慢还是 TCP 真断了。后端慢就调 proxy_read_timeout;TCP 断要查防火墙、conntrack 表是否爆。
故障 3:upstream prematurely closed connection
后端在响应过程中主动关连接。常见原因:后端 keepalive 超时短于 nginx 配置;后端进程 OOM 被 kill;后端配置了 max_request 触发回收。
故障 4:sub_filter 不生效
三个排查:响应是不是 gzip(用 curl -i 看 Content-Encoding);MIME 类型在不在 sub_filter_types 里;sub_filter_once 是不是漏了 off(默认 on 只替换第一个匹配)。
故障 5:后端拿不到客户端真实 IP
没配 X-Real-IP 与 X-Forwarded-For 头。如果配了仍拿不到,看后端框架是不是默认信任这两个头——Spring Boot、Gunicorn 都需要显式开启 forwarded headers 处理。
故障 6:SSL 证书 Common Name 不匹配
nginx 转发到后端的 HTTPS 时,proxy_ssl_server_name on 会启用 SNI,让后端的虚拟主机正确分发。如果后端证书是某具体域名而 nginx 用 IP proxy_pass,会触发 SNI 不匹配。
proxy_pass https://backend.internal;
proxy_ssl_server_name on;
proxy_ssl_name backend.internal;故障 7:proxy_redirect 无效
后端用了奇怪的 Location 头格式(比如带端口的绝对 URL Location: http://backend:8080/login),proxy_redirect 默认规则没覆盖。显式写:
proxy_redirect http://backend:8080/ https://$host/my/;故障 8:Host header is required
HTTP/1.1 强制要求 Host 头。如果你 proxy_pass 写的是 IP,nginx 默认 Host 头是 backend 那行的值(即 IP),后端虚拟主机匹配不上某些站点会报错。修复:proxy_set_header Host $host; 把客户端的 Host 透传过去。
常见问题解答
proxy_pass 末尾斜杠到底加不加?
记一句话:proxy_pass 的 URI 部分(包括只有 /)会替换 location 匹配的前缀;不带 URI 完整传递。子目录反代选带斜杠 + URI 替换;整站反代选不带 URI 完整传递。
多个 sub_filter 没生效怎么办?
三个排查:nginx 版本是不是 1.9.4+;响应是不是 gzip 压缩(必须先解压);sub_filter_once 必须设 off 才会替换所有匹配。还有一种隐蔽情况:后端发的 HTML 字符集是 GBK 而 nginx 默认 UTF-8 处理 sub_filter,部分中文字节会触发匹配失败。
WebSocket 连接不上怎么排查?
四步:浏览器 Network 看 WebSocket 请求 status,101 才是握手成功;看 Request Headers 是否有 Upgrade: websocket 与 Connection: upgrade;nginx 配置是否包含 proxy_http_version 1.1 与 map 块;后端是否真的支持 WebSocket(很多 Java 旧版 Tomcat 不支持)。
反向代理后端怎么获取真实客户端 IP?
nginx 端配置 X-Real-IP 与 X-Forwarded-For 头;后端框架开启 forwarded headers 处理(Nginx 反代后多数框架默认不信任 X-Real-IP,得显式打开)。如果有多层代理,后端要按 X-Forwarded-For 列表的最左侧(第一个 IP)取真实客户端 IP。
proxy_redirect 与 sub_filter 的关系?
proxy_redirect 改的是 HTTP 响应头里的 Location 字段(302/301 跳转用);sub_filter 改的是响应体内的 HTML 文本。两者不能互相替代。后端发 302 跳转时改 proxy_redirect,后端发 HTML 时改 sub_filter,两者经常需要同时配置。
upstream 里某台机器挂了,nginx 多久检测出来?
取决于 max_fails 与 fail_timeout 配置。默认 max_fails=1, fail_timeout=10s,意味着第一次请求失败后该后端被标记不可用 10 秒。生产建议 max_fails=3, fail_timeout=30s 让短暂抖动不立刻摘掉机器。要更精确的健康检查需要 nginx-plus 商业版或者 nginx-upstream-check-module 第三方模块。
反向代理后请求体大小为什么受限?
三层限制:nginx 的 client_max_body_size、PHP 的 upload_max_filesize/post_max_size、后端框架的请求体限制。任何一层卡住都会 413。生产部署时这三个值要取一致。
反向代理能否做 grpc 转发?
能。nginx 1.13.10 起原生支持 grpc_pass。配置:grpc_pass grpc://backend;,不要用 proxy_pass。grpc 请求体是 HTTP/2,nginx 也必须 listen 443 ssl http2 启用 HTTP/2。
反代某境外站点为什么打不开?
典型坑:境外站点用 Cloudflare 加速,对 SNI 与 Host 头敏感。nginx 反代时务必带 proxy_set_header Host 原域名 + proxy_ssl_server_name on + proxy_ssl_name 原域名,让 Cloudflare 把 nginx 当正常客户端处理。
反向代理后 cookie 域名怎么处理?
后端发的 Set-Cookie 头里 Domain 字段可能是后端自己的域名(backend.com),浏览器收到后觉得跟当前站点(example.com)不匹配会丢弃 cookie。用 proxy_cookie_domain 改写:
proxy_cookie_domain backend.com example.com;同样的 proxy_cookie_path 改写 cookie 的 Path 字段。
本文标题:《Nginx 反向代理实战指南:proxy_pass 末尾斜杠、proxy_redirect、sub_filter、WebSocket 与 upstream 全场景配置》
本文链接:https://zhangwenbao.com/nginx-proxy.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0