ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nginx反向代理:proxy_pass与proxy_redirect配置实战

Nginx反向代理:proxy_pass与proxy_redirect配置实战 刚在群里又看到有同学问我 nginx 配了proxy_pass后端接口明明有请求记录但页面一直转圈或者 404到底哪里出了问题这种问题我见过太多次了十有八九是路径规则没搞对或者是后端返回了 302nginx 没有正确处理重定向地址。今天就把proxy_pass和proxy_redirect这两个指令彻底讲透把原理、坑点、实战配置一次说清楚。这两个指令属于 nginx 反向代理场景里的“入口”和“出口”控制proxy_pass负责把请求转发给后端proxy_redirect负责修改后端返回的重定向地址。很多人配置 nginx 时只记得写个proxy_pass加后端地址结果一上线就踩坑最常见的两类问题一类就是路径被“吃掉”或“多拼”了一段另一类就是后端跳转地址不对用户被导到了内网地址或者错误端口。这篇文章适合刚接触 nginx 反向代理的人也适合那些配置已经能跑但想把细节抠清楚的后端开发。我会把每个配置背后的原理讲明白再配合实际配置示例和踩坑记录这样你回头看自己的 nginx 配置时心里会非常有底。1. proxy_pass先搞懂它到底在转发什么1.1 一个最简单的反向代理是怎么工作的proxy_pass从名字上理解就是“把请求转交给另一台服务”。最常见的写法就是location /api/ { proxy_pass http://127.0.0.1:8080; }这段配置的意思是所有以/api/开头的请求都会被 nginx 转发到本机的 8080 端口。这里有个关键点要先建立认知nginx 在转发请求时并不是把原始的 HTTP 请求原封不动地丢给后端它会在内部重新构造一个请求。原始请求的 URI、请求头、请求体等内容会被按照 nginx 的规则处理后再发给上游服务器。这意味着URI 的“拼法”完全取决于你proxy_pass的写法而这正是新手最容易翻车的区域。我打个比方nginx 就像公司前台的收发室。proxy_pass决定了前台把信件送到哪个部门但信件上具体怎么写收件人信息还取决于你配置里的细节——比如你是不是在收件部门后面多写了个“转某某人”这就对应了路径上的替换规则。1.2 带 URI 与不带 URI路径结果完全不一样这是我要强调的重中之重proxy_pass后面带的路径有没有那个“尾巴”直接决定了最终转发给后端的 URI 长什么样。不带 URI 的情况location /api/ { proxy_pass http://127.0.0.1:8080; }此时请求http://你的域名/api/user/listnginx 转发给后端的地址是http://127.0.0.1:8080/api/user/list。也就是说location 匹配到的 URI 路径会原封不动保留nginx 只是把请求发到 8080 端口而已。带 URI 的情况location /api/ { proxy_pass http://127.0.0.1:8080/; }注意proxy_pass后面多了个/。此时请求http://你的域名/api/user/listnginx 转发给后端的地址变成了http://127.0.0.1:8080/user/list。神奇的事情发生了/api/这段被替换成了/。更准确地说nginx 的替换规则是把 location 匹配到的路径部分整体替换成proxy_pass中 URI 的部分。再看一个更复杂的例子location /api/ { proxy_pass http://127.0.0.1:8080/v2/; }请求/api/user/list会被转发为/v2/user/list/api/被替换成/v2/。这个规则其实非常直观你可以把 location 的匹配前缀想象成“被替换的区域”把proxy_pass的 URI 部分想象成“新的区域”。两个区域之间就是直接替换的关系。但这里有一个高频误区很多人以为http://127.0.0.1:8080/带个/无伤大雅结果就出现了路径丢失的问题。举个例子你有一个前端项目和后端接口前端通过/api访问后端后端所有接口都是以/api开头那你就不应该在proxy_pass后面加/否则所有接口都会变成根路径下的资源直接 404。所以配置之前先问自己一个问题我希望后端收到的请求路径前缀是什么如果希望原样保留proxy_pass后面就写 ip:端口如果希望替换掉 location 匹配的那段就写 ip:端口/目标前缀。1.3 正则 location 下别带 URI否则直接启动报错还有一个特别容易踩的坑当 location 使用正则表达式时比如location ~ /api/或者location ~* \.do$nginx 对proxy_pass有一个硬性限制——不能带 URI 部分。配置成下面这样nginx -t 直接报错location ~ /api/ { proxy_pass http://127.0.0.1:8080/; # 这会报错 }错误信息是nginx: [emerg] proxy_pass cannot have URI part in location given by regular expression原因不复杂正则 location 的匹配范围难以确定一个“前缀”既然没有明确定义“被替换的区域”自然也就没法安全地执行“URI 替换”逻辑。nginx 为了避免歧义干脆禁止这种写法。遇到需要替换路径的场景正确做法是配合rewrite指令location ~ ^/api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; }这里rewrite先把/api/xxx改写为/xxx然后break停止继续匹配proxy_pass再把重写后的 URI 原样转发给后端。注意这里proxy_pass不带 URI 的原因是避免冲突。如果你配上rewrite又让proxy_pass带 URInginx 依然会报错。正确组合是“rewrite 负责改路径proxy_pass 只管转发”。1.4 用变量动态指定后端地址实际项目中有些场景需要动态决定代理到哪台后端服务器。proxy_pass支持变量形式比如location / { proxy_pass http://$backend_server; }这里的$backend_server可以通过map指令提前定义也可以来自请求参数、cookie 或者 header。比如按用户地区分流map $http_region $backend_server { default 192.168.1.10:8080; east 192.168.1.11:8080; west 192.168.1.12:8080; } server { location / { proxy_pass http://$backend_server; } }不过需要注意一旦proxy_pass使用变量nginx 就没有办法预先解析上游域名对应的 IP等于每个请求都会动态做 DNS 解析。如果你的变量值是一个域名建议配合 nginx 的 resolver 指令并在变量里直接带上端口避免解析失败。如果变量值只是 IP:端口就不存在这个问题。变量写法还有一个常见用途是在内网环境中把端口动态传给后端的某类服务但这种情况经常会遇到proxy_pass变量后面不能直接接 URI 的新版问题比如# 下面这种写法在新版 nginx 中会被拒绝 proxy_pass http://$backend_server/;想动态转发又带 URI 时常见做法还是用 rewrite 配合或者用set变量拼接完整地址。这部分如果你遇到报错优先检查 nginx 版本新版 nginx 对变量 URI 限制更严格了。2. 配 proxy_pass 时必须一起处理的请求头2.1 Host 头不改后端容易判断错域名proxy_pass只负责把请求转发到指定 IP 和端口但请求头里的Host字段默认会被设置为原始请求中的域名还是被改写成后端 IP答案是nginx 默认会将Host头设置为原始请求中的域名但如果你做了某些配置它也可能变成后端的地址。很多人在这里吃过亏。举个例子你部署了一个 Java 应用应用内部基于request.getServerName()生成一些绝对链接地址或者根据域名做多租户判断。如果 nginx 转发后没有正确设置Host头应用会拿到一个内网地址或错误域名生成的链接和跳转 URL 就全乱了。常见的配置方式是显式设置location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; }这样请求头里的Host字段就始终是用户访问时的域名后端应用感知到的域名就是正确的。如果后端还需要区分端口可以写成proxy_set_header Host $host:$server_port;或者更精确地说把$server_port换成实际对外端口。proxy_set_header不只影响Host它本质上是“重写”转发请求的请求头。nginx 转发给后端时默认会带上一些原始请求头但你可能希望自定义。比如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;上面三行是反向代理场景的“黄金三件套”。后端应用拿到X-Real-IP就可以知道用户真实 IP拿到X-Forwarded-Proto就能知道用户是通过 http 还是 https 访问。2.2 请求体大小与超时时间proxy_pass转发的是完整的 HTTP 请求那请求体有没有大小限制有。nginx 处理 request body 有client_max_body_size这个指令默认是 1m。如果你通过 nginx 上传大文件比如超过 1MB不配置的话会直接得到 413 Request Entity Too Large。所以做文件上传类的代理时一定要加上client_max_body_size 50m;这个配置可以放在 http、server 或 location 层级。放在 location 里更精确只对特定路径生效。另一个容易忽略的是超时时间。nginx 和后端建立连接后有proxy_connect_timeout、proxy_read_timeout、proxy_send_timeout三个超时控制。默认proxy_read_timeout是 60s意思是 nginx 读完后端响应头之前的等待时间。如果后端接口执行时间较长比如报表导出、批量任务超过 60s 还没返回nginx 就会主动断开连接客户端看到 504 Gateway Timeout。遇到这种接口按需调大proxy_connect_timeout 30s; proxy_read_timeout 300s; proxy_send_timeout 300s;2.3 配合 upstream 做负载均衡proxy_pass后面既可以写固定的 IP:端口也可以写 upstream 组名。这也是 nginx 实现负载均衡的最常见方式upstream backend_server { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 weight2; keepalive 32; } server { location /api/ { proxy_pass http://backend_server; proxy_http_version 1.1; proxy_set_header Connection ; } }upstream定义了一个后端服务器组proxy_pass转发到这个组名时nginx 会按照负载均衡策略默认轮询把请求分发到组内的各个服务器。这里有一个经常被忽略的性能优化点长连接。默认情况下nginx 与后端建立的是短连接每个请求都重新建连在高并发下开销非常大。开启 keepalive 后nginx 可以复用与后端之间的 TCP 连接大幅降低延迟和资源占用。启用方法就是上面配置里的三行proxy_http_version 1.1; proxy_set_header Connection ;第一行强制使用 HTTP/1.1 协议第二行清空 Connection 头。这是因为后端要支持 keepalive通常需要 HTTP/1.1而默认请求头里的Connection: close会主动关闭连接所以必须清空。3. proxy_redirect解决后端重定向地址“不对”的问题3.1 为什么后端返回的 Location 会错很多后端应用在收到请求后会返回 301/302 重定向响应响应头里带一个Location字段告诉浏览器“你要的资源跑到这个地址去了”。问题来了后端应用自己的视角里它看到的是一个内网地址或者是一个基于后端自身配置生成的地址。比如 Java 应用配置文件里写的是http://192.168.1.10:8080那它返回的Location可能就是Location: http://192.168.1.10:8080/login但这个地址对外是完全不可达的用户浏览器收到这个响应后会尝试跳转到192.168.1.10:8080这在公网环境下直接失败。还有一种情况是后端返回的Location是相对路径或者内部前缀而对外服务要求不同的前缀。比如用户访问的是https://example.com/api/后端 302 跳转到/login但正确的对外地址应该是https://example.com/api/login。如果不管这个Locationnginx 就原样返回给浏览器结果就是重定向跳错了地方。proxy_redirect就是用来修改Location头的。3.2 语法与配置形式proxy_redirect的基本语法是proxy_redirect [default|off|redirect replacement];proxy_redirect off;表示关闭对 Location 的改写。proxy_redirect default;表示使用默认规则即根据proxy_pass的配置自动生成替换规则。proxy_redirect redirect replacement;表示将匹配到的redirect部分替换成replacement。最常见的用法是location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_redirect http://127.0.0.1:8080/ /api/; }这样当上游返回Location: http://127.0.0.1:8080/user/login时nginx 会把它改写成Location: /api/user/login浏览器再跳转访问的就是对外正确路径。注意这里的匹配规则并不要求完全相等而是前缀匹配。nginx 会把Location中以http://127.0.0.1:8080/开头的部分替换成/api/。replacement可以是绝对地址、相对地址或变量。3.3 用 default 偷懒但要注意前提很多人看到proxy_redirect default;觉得很省事但它真的能满足需求吗我们来拆解一下。proxy_redirect default;的行为与proxy_pass是否带 URI 有关如果proxy_pass不带 URI比如proxy_pass http://127.0.0.1:8080;那么default相当于把Location里的http://127.0.0.1:8080替换成原始的协议域名即$scheme://$host。如果proxy_pass带 URI比如proxy_pass http://127.0.0.1:8080/;那么default会更加智能一些它会根据 location 前缀与 proxy_pass URI 的对应关系做替换。举一个具体的例子location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_redirect default; }此时请求http://example.com/api/user/login转发到后端是/user/login。如果后端 302 返回Location: /login由于default知道 location 前缀是/api/而 proxy_pass 的 URI 是/它会把/login重写为/api/login吗在新版 nginx 中default的处理确实会结合 proxy_pass 的 URI 和 location 前缀来推断但我的建议是关键业务场景不要迷信 default显式写出替换规则最稳妥。因为一旦 proxy_pass 使用了变量、或 location 是正则匹配default 的行为就非常难预测。这也是我看到的很多线上事故根源配置用了proxy_redirect default;然后某一天后端服务地址从 8080 换成了 9090或者从 http 换成了 httpsdefault没跟上变化用户就莫名其妙被跳转到内网去了。3.4 正则替换解决更复杂的 Location 改写proxy_redirect还支持正则表达式匹配语法是在redirect部分使用正则并用变量捕获替换proxy_redirect ~^http://192\.168\.1\.10:8080/(.*)$ /$1;这样不管后端Location返回的是http://192.168.1.10:8080/user/login还是http://192.168.1.10:8080/api/order/list都会被改写成/user/login、/api/order/list变成相对路径后浏览器会基于当前域名自动拼接。正则方式在两种场景下特别实用。第一种是后端的域名或端口经常变动用正则把固定的ip:端口统一“剥掉”第二种是希望把所有 302 跳转都收敛成相对路径由浏览器基于当前访问地址自行拼接降低配置复杂度。我个人的习惯是线上配置一律显式写清楚替换规则不依赖 default不依赖含糊的匹配这样后面接手的人一看配置就能明白意图。4. 一套可以直接抄的完整配置模版4.1 前后端分离项目的标准反代配置前面把两个指令的原理都讲完了下面给出一套我在实际项目中常用的配置模版你可以直接套用。upstream api_backend { server 127.0.0.1:8080 max_fails3 fail_timeout10s; keepalive 16; } server { listen 80; server_name example.com; # 前端静态资源 location / { root /var/www/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端 API 接口 location /api/ { proxy_pass http://api_backend; proxy_http_version 1.1; proxy_set_header Connection ; 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 30s; proxy_read_timeout 60s; proxy_send_timeout 60s; client_max_body_size 20m; # 显式处理后端重定向地址 proxy_redirect http://127.0.0.1:8080/ /api/; } # WebSocket 代理示例比如实时通知服务 location /ws/ { proxy_pass http://127.0.0.1:8081/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; } }这里使用的核心思路是proxy_pass后不带 URI保留客户端 URI 原样转发并由proxy_set_header把客户端信息完整传递到后端proxy_redirect处理后端出现的外部地址改写确保重定向时用户不会跑到内网。proxy_pass后不带 URI 的好处是路径逻辑简单后端收到的路径与 location 前缀一致排错时一眼能看清。4.2 一个典型的“带 URI 替换”的配置再给一个带 URI 替换的示例用于把前端/app路径映射到后端的根路径。location /app/ { proxy_pass http://127.0.0.1:9090/; proxy_set_header Host $host; proxy_redirect http://127.0.0.1:9090/ /app/; }请求http://example.com/app/dashboard会被转发到http://127.0.0.1:9090/dashboard。如果后端返回Location: http://127.0.0.1:9090/user/loginnginx 会改写为Location: /app/user/login用户在这个地址下继续操作整个路径体系不会乱。这个配置的等价理解是外部路径/app/映射到后端/所以重定向时也需要把后端的/前缀映射回/app/。一句话proxy_redirect 的替换方向与 proxy_pass 的方向是相反的要成对出现。4.3 SSL 证书场景下的额外注意点配置 https 时proxy_set_header X-Forwarded-Proto $scheme;就显得尤为重要。因为后端程序如果做 http 与 https 的强制跳转它判断的依据通常就是请求头里的X-Forwarded-Proto。如果这个头没传即使浏览器实际用的是 https后端也以为你是 http从而生成http://开头的重定向地址。还有一个细节proxy_redirect替换规则的 replacement 也可以带上协议和域名比如proxy_redirect http://192.168.1.10:8080/ https://example.com/api/;这种写法适合明确知道对外 https 地址的情况。但如果你图省事想“跟随当前请求的协议”可以写成proxy_redirect http://192.168.1.10:8080/ $scheme://$host/api/;这里$scheme会取当前请求的协议$host会取当前访问的域名灵活性更高。提示我强烈建议所有配置完成后执行nginx -t检查语法不要直接reload。nginx -t能发现大部分低级错误比如正则语法错误、指令拼写错误、变量引用不存在等。5. 常见问题速查表与排查思路5.1 典型症状背后的问题定位症状一接口能通但返回 404如果后端收到的请求路径“多了一段”或“少了一段”基本就是proxy_pass的 URI 替换问题。比如后端接口实际路径是/user/listlocation 写的是/api/proxy_pass 带上了/v2/结果转发成了/v2/user/list后端没有这个路由自然 404。排查方法很简单打开 nginx 的 access log看它实际转发出去的上游地址或者在后端日志里打印收到的请求 URI比对前端实际请求路径与后端期望路径的差别。症状二页面提示 302 跳转到http://192.168.x.x:8080这就是典型的proxy_redirect没有配置或配置不对。后端返回的Location是内网地址nginx 原样返回给浏览器。解决方法是显式加上proxy_redirect把后端内网地址改写成对外可访问的地址。可以直接把内网地址替换成空或相对路径。症状三页面能打开但后台调用接口时提示跨域或域名不对大概率是proxy_set_header Host没有设置。后端应用用Host头来感知当前访问域名如果 nginx 转发时保留了原始 Host 或者改成了 IP后端生成的绝对链接就都错了。建议在 location 里显式配置proxy_set_header Host $host;。症状四上传文件报 413这是client_max_body_size不够。默认是 1m上传大文件时一定要调整同时前端也要有对应提示。5.2 配置排查的标准操作流程遇到 nginx 反代问题我一般按照下面的顺序排查第一步先看 nginx 配置是否有语法错误nginx -t第二步检查 nginx 的 access error log重点看upstream相关的日志和proxy_pass实际转发地址tail -f /var/log/nginx/access.log tail -f /var/log/nginx/error.log第三步直接 curl 内网后端接口确认后端是否正常curl -v http://127.0.0.1:8080/api/user/list第四步对比“直接访问后端”和“通过 nginx 访问”的返回结果差异。通常会集中在路径、Host、重定向地址这几个方面。5.3 问题速查表症状可能原因解决方案接口全部 404proxy_pass 带 URI 导致路径前缀被替换去掉 proxy_pass 的 URI 部分或使用 rewrite 手动重写路径302 跳到内网 IP:端口proxy_redirect 未配置或配置错误显式添加 proxy_redirect 将内网地址改写成对外地址登录后跳回首页或死循环重定向地址错误未保留正确前缀检查 proxy_redirect 替换规则是否与 proxy_pass 方向一致后端日志记录的用户 IP 都是 nginx 地址缺少 X-Real-IP / X-Forwarded-For 设置添加 proxy_set_header X-Real-IP $remote_addr; 等配置应用返回的绝对链接协议不正确缺失 X-Forwarded-Proto 头添加 proxy_set_header X-Forwarded-Proto $scheme;上传大文件报 413client_max_body_size 太小调大 client_max_body_size后端接口处理超时报 504proxy_read_timeout 太短调大 proxy_read_timeout高并发下性能差未开启到后端的 keepalive配置 upstream keepalive 并启用 HTTP/1.1 长连接排查时如果使用浏览器开发者工具重点关注网络面板中 302 响应的Location头。很多问题一眼就能看出是后端返回了http://192.168.x.x还是返回了相对路径但 nginx 没做任何改写。5.4 我的几条实操心得第一写 proxy_pass 之前先画一条“路径变换链路”。比如外部请求是/api/user/list我期望后端收到的是/api/user/list那 proxy_pass 就写http://后端不带 URI。我期望后端收到/user/list那 proxy_pass 写http://后端/。这条链路想清楚配置基本不会错。第二proxy_redirect 写在哪就说明哪个层级会出现重定向。后端如果根本不返回 302或者只返回 JSON 数据那 proxy_redirect 写不写都无所谓但如果后端是 Web 应用、会做登录跳转一定要关注重定向地址。我遇到过好几个项目功能一切正常就是登录后跳转地址不对最后都是 proxy_redirect 漏配导致的。第三所有 nginx 配置都要进版本管理。配好之后把配置文件提交到 git说明写清楚每个 location 和每个 proxy 参数的目的。这样即使半年后配置出了问题你还能从历史记录里看出当初为什么这么写。第四尽量少用 default多用显式规则。虽然proxy_redirect default看起来省事但在复杂的路径映射下default 的行为并不直观。尤其是当你在一个 server 块里配置了多个 location、不同前缀映射时default 很容易“过拟合”某一个配置然后在别的地方产生意外行为。第五每次改完配置先 reload 再观察一段时间不要在大流量时段做激进修改。nginx 的 reload 是平滑的不会中断已有连接但配置错误时 reload 不会生效nginx 会继续用旧配置运行这点要格外注意避免误以为配置已经更新。个人在实际操作中最大的体会是nginx 反代配置本质上是“路径和头的翻译流程”。proxy_pass负责翻译请求路径proxy_set_header负责翻译请求头proxy_redirect负责翻译响应头。只要把这三类“翻译规则”理清楚线上 90% 的反代问题都能自己排查出来。这篇文章里的配置和思路都是我在生产环境反复验证过的你照着配置一遍再结合自己的业务场景做调整基本不会走弯路。
返回列表