☰
Nginx proxy_pass 路径改写与反向代理配置详解
2026/9/26 3:37:03 网站建设 项目流程

我第一次在location里写proxy_pass的时候,犯过一个特别蠢的错:location /api/ { proxy_pass http://127.0.0.1:8080/; },看起来人畜无害,结果前端请求/api/user/list,后端接口收到的却是/user/list,一截/api就这么凭空消失了。当时后端代码又对路径有强校验,接口直接 404,排查了大半天才意识到是proxy_pass尾部那个斜杠在作怪。Nginx 的proxy_pass指令,本质上就是反向代理里的转发动作,它决定一个请求被交到哪台服务器、以什么路径交过去。这篇内容就围绕proxy_pass的日常使用展开,适合刚把 Nginx 跑起来、准备给后端服务统一入口的人,也适合那些配置过反向代理但被路径改写、404、502 搞得头大的同学。

1. 先搞明白:proxy_pass 转发的是请求,还是路径改写

1.1 一次请求在 Nginx 里的完整旅程

proxy_pass不是独立运转的,它必须配合location才有意义。一个 HTTP 请求到达 Nginx 后,大致会走这几步:先按端口匹配到server块,再按 URI 从高到低匹配location,最后执行location内的指令。proxy_pass就是那个“执行”环节,它告诉 Nginx:把当前请求转给某个上游服务器。

但问题恰恰出在“转给”这两个字上。很多人以为只要写上proxy_pass http://127.0.0.1:8080;,请求就会原封不动地到127.0.0.1:8080。实际上,Nginx 在转发前会做一次 URI 拼接,拼接规则取决于proxy_pass后面有没有带 URI(也就是斜杠后面的那部分)。这就像快递转寄:地址可以照着原包裹改,也可以把收件人信息整个换掉,区别就在你填转寄单的时候有没有多写一个“收件人”。

1.2 为什么有人配完没事,有人一配就 404

两种写法的行为差异,是很多“幽灵 404”的根源:

  • proxy_pass后面不带 URI,请求的 URI 原样透传给上游;
  • proxy_pass后面带 URI(哪怕只有一个/),location 匹配到的那一段前缀会被替换掉。

举个例子。你有个后端服务,接口路径本身是/user/list。如果 Nginx 配置成:

location /api/ { proxy_pass http://127.0.0.1:8080; }

不带 URI,后端收到的还是/api/user/list。要是后端根本没有/api这个前缀路由,自然 404。反过来,如果你想让前端用/api访问、后端去掉前缀,写成:

location /api/ { proxy_pass http://127.0.0.1:8080/; }

后端收到的是/user/list,正好匹配。两种配置只有尾部一个斜杠之差,结果天壤之别。

1.3 什么时候用变量会改变一切

还有一种写法是proxy_pass使用变量,比如:

location / { proxy_pass $backend_url; }

一旦用了变量,Nginx 就不做 URI 替换了,直接把原始请求 URI 透传上去,而且运行时需要resolver来解析域名。这个坑我后面单独讲,反正记住一句话:能用静态写法就别用变量,动态转发场景才需要它。

2. location 与 proxy_pass 连用时的 URL 变化对照,建议收藏

这一节我直接给结论、给表格,方便你排查时对照。先说核心规则:

判断标准只有一个——proxy_pass后面有没有 URI(斜杠及之后的内容)。有,就把location匹配到的那段前缀替换掉;没有,就原样透传。

2.1 五种写法的实际结果

假设请求路径都是http://example.com/api/user/list,location是/api/:

location 配置proxy_pass 写法上游实际收到的 URI说明
location /api/http://backend/api/user/list不带 URI,原样透传
location /api/http://backend//user/list带/,替换匹配前缀
location /api/http://backend/new//new/user/list/api/被替换成/new/
location /api/http://backend/api//api/user/list替换后看起来和原来一样
location /api(无尾斜杠)http://backend//api/user/list会变/user/list,但/api单独访问会触发 301注意 location 尾斜杠差异

很多人栽在最后一行:location /api和location /api/不是一回事。/api不带斜杠,既能匹配/api也能匹配/api/user/list;而location /api/只匹配以/api/开头的路径。配置location /api时如果顺手写了proxy_pass http://backend/,那https://example.com/api这种不带尾斜杠的访问会被 Nginx 301 到/api/,而/api/user/list会被剥掉/api前缀变成/user/list。路径一变,后端路由就乱了。

2.2 正则 location 的特殊限制

用~或~*写的正则 location,proxy_pass后面不能带 URI,否则 Nginx 启动直接报错:

"proxy_pass" cannot have URI part in location given by regular expression

原因不难理解:正则匹配的不是固定前缀,Nginx 不知道该怎么“替换匹配段”。如果非要在正则 location 里做路径改写,就得加变量,或者改用rewrite先改路径再代理。日常项目里,我建议能用前缀 location 解决的问题就不要上正则,配置可读性和可维护性都会好很多。

2.3 自己动手验证转发结果

判断配置到底转成了什么,最直接的办法是看后端日志。不过后端正忙的时候不好翻,我更推荐先用curl本地模拟:

curl -H "Host: example.com" http://127.0.0.1/api/user/list -v

然后在后端服务打印收到的请求路径,一眼就能看出 URI 被改成了什么。如果后端不方便打日志,还可以临时在后端机器上用nc -l 8080监听端口,再发起一次请求,看原始请求行里的路径。这个土办法在排查 404 时特别好用。

3. 三类典型场景配置,从入门到能上生产

3.1 最简单的端口转发:把 3000 端口挂到 80 上

很多人的第一个代理需求,就是把本机某个服务端口暴露到 80。比如 Node 服务跑在127.0.0.1:3000,想用http://example.com/app访问:

server { listen 80; server_name example.com; location /app/ { proxy_pass http://127.0.0.1:3000/; 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_pass http://127.0.0.1:3000/;会剥掉/app/前缀,后端收到/开头的路径。如果你希望后端也保留/app/前缀,就把斜杠去掉,改成proxy_pass http://127.0.0.1:3000;。

为什么建议加上三行proxy_set_header?因为后端拿到请求后,经常需要知道真实客户端 IP 和原始 Host。如果不传,后端看到的来源永远是 Nginx 自己的地址,做日志分析、限流、用户追踪都会出现问题。

3.2 去掉前缀的转发:前端带/api,后端不含/api

这是前后端分离项目最常见的诉求。前端统一请求/api/...,后端接口却只认/...,于是:

location /api/ { proxy_pass http://backend_server:8080/; }

/api/user/login到了后端变成/user/login。这个例子看起来简单,但我见到的生产事故里,至少有三分之一是有人多加了一层路径导致后端 404。比如后端接口其实是/user/login,你却在proxy_pass里写了http://backend_server:8080/api/,那后端收到的就是/api/user/login,又回到原点了。

3.3 多个后端共用一个 Nginx 入口

服务器上往往不止一个服务。Nginx 通常作为统一入口,按路径分发到不同后端。这块配置建议把所有上游先定义好:

upstream user_service { server 127.0.0.1:8081; keepalive 32; } upstream order_service { server 127.0.0.1:8082; keepalive 32; } server { listen 80; server_name example.com; location /user/ { proxy_pass http://user_service/; proxy_http_version 1.1; proxy_set_header Connection ""; } location /order/ { proxy_pass http://order_service/; proxy_http_version 1.1; proxy_set_header Connection ""; } }

这里把user_service和order_service定义成 upstream,好处是以后扩容只要在 upstream 里加一行server就行,不用到处改proxy_pass。另外一个细节是proxy_http_version 1.1和Connection "",没有这两行,upstream 里配的keepalive根本不生效,因为默认 HTTP/1.0 的短连接无法复用。Nginx 到后端的连接频繁重建,高并发下会看到大量 TIME_WAIT,性能明显下滑。

3.4 代理到 HTTPS 上游

现在很多内网服务也上了 HTTPS,Nginx 代理到上游时要注意证书校验问题。如果上游是自签名证书,Nginx 默认会校验并失败,需要在location里配:

location /secure/ { proxy_pass https://internal-server/; proxy_ssl_server_name on; proxy_ssl_verify off; }

生产环境里除非是内部可控证书,否则不建议关掉校验。proxy_ssl_server_name on的作用是让 Nginx 在 TLS 握手时发送 SNI,上游如果按域名做虚拟主机就必须开。这个字段虽然不在proxy_pass本身,但代理 HTTPS 服务时逃不掉,顺便说一句。

4. 生产环境踩坑实录:三个和 proxy_pass 有关的故障排查全过程

4.1 502 Bad Gateway,问题不一定在后端

有一次服务半夜告警,前端全部 502。我第一反应是后端挂了,systemctl status一看,后端进程活得好好的,端口也在监听。再手动curl http://127.0.0.1:8080/health,响应正常。那问题就在 Nginx 和后端之间。

打开 Nginx 错误日志,通常在/var/log/nginx/error.log,看到这么一行:

connect() failed (111: Connection refused) while connecting to upstream

“Connection refused”说明连接被拒绝,也就是后端端口虽然是监听状态,但拒绝新连接。这种情况在处理高并发时很典型:后端连接数打满,或 Nginx 配置的proxy_pass连到了错误端口。后来一查,是部署脚本把后端服务的实际端口从 8080 改成了 8081,Nginx 配置没同步更新。

排查 502 的顺序我建议固定下来:先看后端进程和端口,再curl后端本机地址,最后看 Nginx 错误日志。如果错误日志里是upstream prematurely closed connection,那通常是后端主动断开,常见原因是后端超时设置比 Nginx 的proxy_read_timeout短,或者 keepalive 配置不匹配。这时候调整proxy_read_timeout、proxy_send_timeout往往就解决了。

4.2 404 的根源是路径被悄悄改写

另一个印象深刻的故障是:前端请求/api/v1/order/detail,后端日志里看到的是/v1/order/detail,导致路由匹配不上直接 404。这就是典型的proxy_pass尾部斜杠问题——location /api/配了proxy_pass http://backend/,/api/前缀被剥掉了。

事后复盘,配置的人本意是想转发到/api/v1/order/detail,而不是剥掉前缀。正确的写法有两个方向:

  • 如果后端接口确实带/api/v1,那应该写proxy_pass http://backend;(不带 URI),原样透传;
  • 如果后端不想要/api,只想要/v1/order/detail,那就应该调整location的匹配粒度,而不是让proxy_pass尾斜杠去做删除操作。

排查这类问题,最快的办法是看后端访问日志里的请求行,确认实际到达的路径。很多 404 并不是代码 bug,而是路径在 Nginx 这一层被改得不符合预期。每次改完proxy_pass,我都习惯性用nginx -t先做语法检查,再实际请求一次,根本不费多少时间。

4.3 无限 301:proxy_pass 指回了自己

还有一种特别隐蔽的坑:配置了proxy_pass后,浏览器访问某个路径,地址栏一直跳转,刷几次都停不下来,最后浏览器报“重定向次数过多”。打开 Nginx access log 一看,全是 301,Location 指向的还是当前域名。

最典型的成因有两个。

一是location写法问题。比如:

location /app { proxy_pass http://127.0.0.1:8080/; }

当用户访问http://example.com/app(不带尾斜杠)时,Nginx 发现location /app匹配到了,但代理后又想保留原路径,于是先 301 到/app/,再进入代理逻辑。可如果后端的路由根本不含/app/,跳完还是 404,甚至有的配置会在/app/和/app之间反复横跳。

二是proxy_pass的域名解析指向了 Nginx 自己。比如配置:

location / { proxy_pass http://example.com; }

example.com恰好也解析到这台服务器,请求进来后又代理回 Nginx 本身,而 Nginx 又没匹配到合适的location,就会陷入循环。排查这种问题,看响应头里的 Location 字段最直观:如果 Location 里的域名还是自己的域名,基本就是代理链路里出现了自我指涉。解决方法是明确区分对外域名和内网代理地址,或者用 upstream 指向实际后端 IP。

4.4 变量写法在运行时才报错:no resolver defined

proxy_pass用变量会引入额外限制,我前文提到过。实际遇到的报错类似这样:

no resolver defined to resolve backend.service.consul

原因简单:Nginx 启动时不会立刻解析变量里的域名,等请求到来它才去解析,可你没配resolver,它就不知道怎么查 DNS。解决方法是加上:

resolver 127.0.0.53 valid=30s;

或者干脆把域名换成 IP。如果不是做服务发现、动态路由这种场景,我非常不建议用变量写proxy_pass——它绕过了 Nginx 推荐的安全校验,还容易踩 resolver 的坑,出了问题也难排查。

5. 让 proxy_pass 更稳的搭配:这几个配套指令别漏掉

5.1 代理头信息:Host 与真实 IP

一个完整的反向代理配置,基本都会带上这几行:

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;

为什么这么重要?后端做日志统计、限流、权限校验时,通常依赖真实客户端 IP。如果 Nginx 不做X-Forwarded-For透传,后端拿到的全是 Nginx 内网地址,几万用户看起来就像同一个 IP 在访问,限流策略直接被击穿。Host头也很关键,很多后端服务按域名做虚拟主机路由,如果Host没传对,请求会落到错误的站点上。

5.2 让 Nginx 与后端保持长连接

对高并发服务,Nginx 与后端之间频繁建立 TCP 连接非常浪费资源。我习惯给每个 upstream 加keepalive,然后在location里配合两个指令:

upstream backend { server 127.0.0.1:8080; keepalive 32; } location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; }

keepalive 32表示 Nginx 会为每个 worker 进程保持最多 32 个空闲长连接到后端。Connection ""是清空请求头里的 Connection 字段,避免默认的close。加了这两处,后端建立的连接可以复用,压测下 QPS 能提升不少,TCP 连接数也会明显下降。

5.3 proxy_redirect:处理后端返回的重定向

后端接口偶尔会返回 301/302,Location 头里带着后端自己的地址。如果后端地址是内网地址,客户端拿到后会直接访问内网地址,当然访问不到。这时用proxy_redirect重写:

proxy_redirect http://backend:8080/ /;

把后端返回的Location: http://backend:8080/some/path改写成相对路径/some/path,客户端就会乖乖走 Nginx 入口。这个指令平时容易被忽略,但一旦后端做登录跳转、OAuth 回跳,没配它基本上必出问题。

5.4 我落地的几个小习惯

写proxy_pass这几年,我沉淀了一些固定习惯,聊当参考:

  1. 写每一条proxy_pass前,先在纸上把 URL 走一遍:用户输入 →location匹配 →proxy_pass改写 → 后端收到的路径。不要凭感觉写。
  2. 每份配置落库前必跑nginx -t语法检查,再用nginx -T查看最终生效的完整配置,防止加载的不是你以为的那份文件。
  3. 本地测试时用curl -H "Host: example.com"模拟真实域名访问,因为不同server_name下proxy_pass行为完全可能不一样。
  4. 后端路由比较“挑”的时候,尽量让proxy_pass不带 URI,路径改写用rewrite或专门的网关层处理,避免在 Nginx 里做太多隐式操作。

最后分享一个个人体会:proxy_pass看起来只是几行配置,但它决定了你的服务是被正确送达还是拐进死胡同。很多线上问题追根究底,都是对“带不带 URI”这个细节理解不透。花半小时把本文这个规则刻进脑子里,配合curl、日志和nginx -T这几个工具,以后再遇到反向代理的怪问题,基本都能快速定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询