做 Java 后端的人基本都跟 Nginx 重定向往打过交道。本地接口一切正常发到测试环境之后访问旧地址突然被带到登录页或者直接 404你打开浏览器控制台一看响应码 301Location 却指向内网 IP你以为是 Java 代码里的跳转写错了改了几个小时最后一查问题出在 Nginx 配置。这类问题绕开了业务逻辑发生在前置 Nginx 层排查起来特别考验对整个请求链路的理解。这篇文章只聊一件事Nginx 配置重定向。我会把 Java 后端视角下最常用的重定向写法、最容易踩的坑、能直接抄的配置和排查命令全部整理出来。不管是 Spring Boot、SSM 还是传统 Servlet 项目只要前面挂着一台 Nginx这篇文章都值得存下来。下面从最基础的“谁在做重定向”开始一层一层把它讲透。1. 配置重定向前先分清 Nginx 和 Java 到底谁在做跳转1.1 Java 端重定向和 Nginx 端重定向的本质区别重定向的本质是服务端返回一个 301 或 302 响应并且带上 Location 头告诉浏览器“你要的资源不在这去那边”。Java 端可以用response.sendRedirect()实现Nginx 端可以用return 301实现但两者对请求链路的消耗完全不同。Java 端的重定向请求已经完整到达业务服务Tomcat 容器、Filter、拦截器、Controller 全部跑了一遍之后才能拿到跳转结果Nginx 端的重定向则在进入 Java 之前就拦截返回Servlet 容器连碰都不会碰开销几乎可以忽略。这个本质区别直接决定方案选型。域名迁移、HTTP 强制转 HTTPS、老链接 301 到新链接这些“只改入口、不改业务内容”的操作放在 Nginx 层永远比放在 Java 层划算。而登录失效跳登录页、支付完成后跳回商户页面、权限不足跳无权限提示页这些跳转依赖业务状态必须放在 Java 层。我见过有团队把登录判断写进 Nginx rewritecookie 解析和维护逻辑在配置里重复实现每次需求变更都要跟一段 nginx 正则较劲这种归属错位绝对是在埋雷。1.2 重定向返回的 Location 是怎么生成的决定了很多坑一个重定向要真正生效核心是 Location 头最终落到正确的地址。Nginx 生成 Location 时如果配置里写的是绝对 URL就直接使用如果写的是相对路径Nginx 会拿当前请求的协议、Host、端口拼成绝对地址。问题往往出在第二步Nginx 认为的协议和域名不一定等于浏览器访问的原始协议和域名。在典型的 Java 部署架构里浏览器访问https://api.example.comNginx 收到请求后以内网方式转发给http://192.168.1.10:8080此时如果 Java 响应了sendRedirect(/login)Nginx 会拿它认为的主机信息拼接 Location最后可能变成http://192.168.1.10:8080/login用户点进去直接连接失败。这一类问题我在后文会反复提到修复手段要么靠 proxy_redirect要么靠转发头设置要么靠 Java 框架层面的协议识别没有一个通用银弹必须链路完整地看。1.3 完整链路浏览器、Nginx、Java 三方如何串起来新手排查重定向问题我的第一个建议是先画时间线。浏览器发出请求Nginx 根据 location 块的匹配规则决定是直接返回静态内容、执行 rewrite还是 proxy_pass 转发给 Java 应用Java 应用处理完业务可能返回一个带 Location 的响应Nginx 再把响应交还给浏览器。每一层都可能改写 Location 的协议、域名、路径、查询串排查慢不是能力问题而是信息链路太长。我每次做重定向变更前都会用不带跟随的 curl 命令把中间步骤暴露出来比如curl -I http://api.example.com/old-path只显示响应头和当前状态码不自动展开后续跳转。看到 301 或 302 之后再去请求 Location 指向的地址一步一步验证。很多人习惯用curl -I -L一次看完结果中间某一跳出了错最后却看到一个看似正常的 200反而把真正的错误藏住了。2. 重定向配置的四个常用写法选型比硬记指令更重要2.1 return 301 和 return 302越简单越要注意 Location 的写法Nginx 的 return 指令是重定向首选写法最简单server { listen 80; server_name api.example.com; return 301 https://api.example.com$request_uri; }return 301后面的跳转地址建议写成绝对 URL。我见过不少人写成return 301 /new/;这在单机 Nginx 下没问题Nginx 会自动拼接主机名生成绝对地址但如果 Nginx 前面还有云负载均衡之类的转发设备拼接出来的地址就可能带上内网 IP跳到根本不存在的机器上。写成绝对 URL 之后Location 始终是一个可以直接理解的地址排障成本低很多。另一个细节是$request_uri的作用它保留原始请求路径和全部查询参数避免跳转后参数丢失。要注意$request_uri和$uri不一样前者是原始请求的完整 URI后者是经过 Nginx 规范化之后的路径做重定向保留参数时优先$request_uri。注意location 块里写 return 时这个 location 内的其他指令不会继续执行包括 proxy_pass 和 rewrite。这个特性既是优点也是坑确认规则顺序时一定要想清楚。302 表示临时跳转浏览器下次访问旧地址时仍会回到后端重新拿 Location301 表示永久跳转浏览器和搜索引擎都会把结果缓存下来。这一点决定了一个非常实用的选型线上环境不确定跳转规则会不会频繁调整时先用 302确认稳定后再切 301否则每次改规则都要等用户浏览器缓存过期。2.2 rewrite 的正则迁移与 last、break、permanent 的含义rewrite 适合做旧链接到新链接的规则化跳转典型写法location /old/ { rewrite ^/old/goods/([0-9])\.html$ https://api.example.com/new/goods/$1 permanent; }permanent相当于 301redirect相当于 302。正则里的([0-9])可以捕获商品 ID$1把它带到新地址里整站旧路径可以在一段配置里收敛完。但 rewrite 有个很折磨人的行为执行完一条 rewrite 后如果后面没有last或breakNginx 会拿着改写后的 URI 重新匹配 location 块。举个例子rewrite ^/old/(.*)$ /new/$1;之后Nginx 可能继续去命中/new/的 location再执行一段代理逻辑如果你本意是直接返回跳转结果却多走了一个层级最终出现 404 或重复代理就是这个原因。我的建议是单纯全站跳转优先用 return只有路径规则确实复杂、需要正则捕获时才用 rewrite并且规则末尾明确加break或last避免规则链多落点。2.3 proxy_redirect反向代理场景下修正上游 LocationJava 后端配合 Nginx 最常见的故障是 Java 应用生成的 Location 里带的是内网地址。比如 Java 服务跑在http://127.0.0.1:8080外网域名是https://api.example.comJava 某一个接口执行到sendRedirect返回的 Location 带着内网地址或错误端口。Nginx 默认虽然会尝试根据 proxy_pass 关系做一次改写可一旦上游地址、Host 头、外层代理三层信息不一致默认行为就会产生不可预测的结果。针对这种情况显式声明改写规则是最可靠的proxy_redirect http://127.0.0.1:8080/ /;这条指令的作用是把上游返回的 Location 中前缀为http://127.0.0.1:8080/的部分替换成/最终落到浏览器时变成相对路径浏览器会基于访问域名自动拼接。注意 proxy_redirect 只处理响应头里的 Location 和 Refresh 字段不负责改写响应体里的文本如果 Java 代码里把绝对地址直接拼进了 JSON 或 HTMLNginx 无能为力必须改代码。这也是我一直强调“重定向问题要前后端一起看”的原因。2.4 absolute_redirect 和 server_name_in_redirect 两个开关Nginx 有两个开关用来控制相对重定向地址如何转成绝对地址新手基本不知道但在多层代理场景里非常实用。absolute_redirect决定 Nginx 是否生成绝对 URLserver_name_in_redirect决定拼接绝对 URL 时用 server_name 指令里的名称还是用请求头里的 Host。默认情况两者都偏向“绝对化”于是会出现一个很讽刺的结果服务器对外域名是api.example.comserver 块却写着server_name localhost;Nginx 生成的 Location 就变成http://localhost/xxx。如果你有一批域名要收敛或者 Nginx 前面还有其他转发层可以在 server 块中加两行absolute_redirect off; server_name_in_redirect off;开了之后 Location 会尽量保持相对路径最终拼接交给浏览器处理。这个方法在前后端分离项目里实测效果很好用户在哪个域名进来跳转就停留在哪个域名体系内不会因为一层代理就跳到内网地址。2.5 快速选型对照表场景推荐写法一句话原因全站 HTTP 跳 HTTPSreturn 301简单直接开销最小老路径迁移新路径rewrite ... permanent能正则捕获参数表达力强Java 返回的 Location 带内网地址proxy_redirect显式改写响应头前缀多层代理下的域名收敛absolute_redirect off让浏览器按访问域名拼接跳转规则还不稳定先用 302 再切 301避免浏览器长期缓存错误跳转3. Java 后端与 Nginx 配合时重定向的三个关键坑3.1 请求协议判断X-Forwarded-Proto 比 getScheme 更可信Java 端判断请求是不是 HTTPS很多人第一反应是request.getScheme()或者request.isSecure()。这两个方法读取的是 Servlet 容器看到的协议而容器看到的协议取决于 Nginx 转发时的设置。如果 Nginx 没有配置任何转发头Tomcat 收到的就是一个普通 HTTP 请求getScheme()返回http。这时候 Spring Security 之类的框架如果基于当前请求动态生成重定向地址就会生成一个 http 地址用户在浏览器里的体验就是“明明访问 HTTPS跳转后却掉回 HTTP”。标准做法是 Nginx 加一行转发头把真实协议传下去proxy_set_header X-Forwarded-Proto $scheme;Java 侧读取request.getHeader(X-Forwarded-Proto)作为判断依据。Spring Boot 场景下还可以配置server.forward-headers-strategyframework或native让容器自动识别标准转发头最终让getScheme()返回正确值。不同版本的 Spring Boot 对这些策略的支持有细微差别我一般在 2.x 以上版本用framework如果你用的是老版本最好先在测试环境打印一遍相关 header 确认。3.2 Spring Boot 里 sendRedirect 和 RedirectView 的注意点Spring Boot 里写重定向有几种常见姿势Controller 直接返回redirect:/new-page代码里调用response.sendRedirect(/new-page)或者用RedirectView手动构造。这些本身没问题问题通常出在最终生成的 Location 上。Servlet 容器拼接绝对地址时会依据请求的 Host 头而 Host 头到底是 Nginx 传递的外网域名还是内网地址完全取决于 Nginx 有没有proxy_set_header Host $host;。我习惯把下面这段作为后端代理的标配location /api/ { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://java-app; }这样一来外部访问api.example.com/api/xxxJava 的 Controller 里做/order/detail/100这类相对重定向时容器基于正确的 Host 拼接出http://api.example.com/order/detail/100而不是192.168.1.10:8080。另一个细节是保留参数跳转地址里如果有拼参需求优先用相对 URL 并附带 query避免自己用字符串拼http://IP:端口/路径这种脆弱的组合。3.3 过滤器与拦截器做强制跳转时先确认会否和 Nginx 打架权限校验类重定向常常放在 Filter 或拦截器里未登录访问受保护接口直接response.sendRedirect(/login)。如果 Nginx 对/login这个路径还配置了其他跳转规则比如强制 HTTPS 或域名迁移那完整链路就变成了多段跳转Nginx 先跳一次Filter 再跳一次最终用户看到的地址和预期完全不一致。更麻烦的是如果 Nginx 对未登录接口配置了缓存Filter 里的判断根本不会执行用户一直看到旧页面。排查时我会把浏览器开发者工具里的整个重定向链按顺序列出来然后判断每一跳到底是 Nginx 干的还是 Java 干的。判断方法很简单Location 带 context path、session 参数、业务参数的多半是 Java 层写入协议变化、域名变化、整站跳转的多半是 Nginx 层。分清楚归属之后再决定改哪一侧能少走很多弯路。4. 完整实战HTTP 强制跳转 HTTPS 并保留查询参数4.1 场景需求与假设最近给一个电商后台项目调整部署架构需求拆开是这样的80 端口所有请求必须 301 到 443同时不能丢查询参数旧的登录页/admin/old-login.html要永久迁移到新登录页/admin/login迁移时保留原有参数Java 网关服务跑在127.0.0.1:8080对外全部走 Nginx。很多人改重定向时会忽略查询参数。比如原地址是/admin/old-login.html?sourcewxchannelshare如果只写成return 301 https://$host/admin/login;用户跳过去之后source和channel全部丢失后面接单系统统计不到渠道来源。这种问题特别隐蔽因为主页跳转看着正常只有带着特定参数的深链访问才会暴露。4.2 配置全文与逐条解释这是实际使用的核心配置server { listen 80; server_name api.example.com; # 全站请求统一跳 https保留原始路径和查询串 location / { return 301 https://$host$request_uri; } # 旧登录页精确迁移参数单独携带 location /admin/old-login.html { return 301 https://$host/admin/login?$args; } } server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/nginx/ssl/example.pem; ssl_certificate_key /etc/nginx/ssl/example.key; absolute_redirect off; server_name_in_redirect off; # 登录页代理到 Java 服务 location /admin/login { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8080; } # 业务接口代理 location /api/ { proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8080; } }解释几个关键点。第一段 server 里用了两个 locationlocation /是兜底所有走 80 端口的请求都会跳 HTTPSlocation /admin/old-login.html是精确匹配优先级高于前缀匹配保证这条旧路径走专用跳转规则而不是被普通规则提前拦截。return后面的跳转地址里$host取的是请求头里的 Host不随 server_name 变化即使通过内网 IP 访问也不会拼出错误域名$request_uri保留原始 URI 的全部内容。第二段 server 里我专门加了absolute_redirect off; server_name_in_redirect off;。原因前面说过这台 Nginx 前面还有一层负载转发设备默认情况下 Nginx 生成 Location 时可能带内网地址。两个开关关掉以后Nginx 返回的定位地址尽量保持相对路径由浏览器按访问域名解析问题就消失了。还有一点值得展开每个 location 都带proxy_set_header Host $host;和proxy_set_header X-Forwarded-Proto $scheme;。前者保证 Java 端收到正确 Host后者保证 Java 知道当前协议是 HTTPS。这两行配合 Java 侧的统一协议识别策略能同时解决“回跳 http”和“Location 指向内网”两个问题。4.3 用 curl -I 验证整条重定向链路配置完先跑nginx -t确认语法没问题后再nginx -s reload。不要直接开浏览器验证浏览器会自动跟随跳转把中间状态藏起来先用命令行看原始响应最靠谱。curl -I http://api.example.com/admin/old-login.html?sourcewx返回里盯着三点看状态码是不是 301Location 是不是完整的https://api.example.com/admin/login?sourcewx响应头里有没有被中间层意外缓存。确认旧登录页没问题之后再验证普通路径curl -I http://api.example.com/api/order/100这里要确认 Nginx 按预期返回 301 到 https并且路径和参数完整。接着测 https 直达curl -I https://api.example.com/api/order/100正常情况下接口应该返回业务状态码而不是再发生重定向。最后再用带-L的方式跟随一次确认最终页面状态码符合预期。整轮验证做完重定向链路里 Nginx 和 Java 两侧的改写基本都被测到了。5. 线上事故记录与问题速查表5.1 重定向死循环Nginx 和 Java 互相踢球线上最典型的事故是浏览器报“重定向次数过多”。那次现象是这样的Nginx 把所有 http 强制重定向到 httpsJava 端又因为读到的是 http 转发头把请求重定向回 http于是 http→https→http→https 无限循环。根因是两个组件对协议判断不一致Nginx 知道访问是 httpsJava 不知道。解决办法前面已经说过Nginx 必须proxy_set_header X-Forwarded-Proto $scheme;Java 侧也要配置 forward-headers 策略让两个进程得出同一个结论。这种死循环问题光看单台服务器日志往往看不出名堂因为每一跳都是正常响应组合在一起才出问题。我会把浏览器地址栏里不断变化的前缀按顺序记录下来用最笨的方法往上游倒推通常十几分钟就能定位到是协议适配脱落。5.2 301 缓存改完配置用户还在旧地址另一次教训是域名迁移时先用 301 做了旧域名全量跳转后来业务想调整新域名前缀结果发现无论怎么改 Nginx很多用户访问旧域名还是跳到最初的目标地址。原因不是 Nginx 配置没生效而是浏览器记住了 301 的永久缓存。301 的缓存是按主机和路径维度记录的用户不清缓存、不换浏览器旧跳转就一直在。经历过这次之后我的规则很简单跳转目标可能还会调整的场景一律用 302只有当跳转规则确认长期不变时才用 301。协议升级、永久域名迁移这类可以切 301灰度发布、AB 测试这类场景坚决用 302。5.3 多级代理下端口和服务地址被吞还有一次是云负载均衡和 Nginx 两层代理叠加Java 返回了一个绝对跳转地址经过两层转发后端口直接消失。现象是用户点击跳转后进入的 URL 没有端口号在非标准端口部署时直接访问失败。排查时先用curl -I逐跳对比 Location定位到第二层代理把 Host 改成了内网地址同时外层的 https 端口又被默认替换成 80最终加了三重修正proxy_set_header Host、proxy_set_header X-Forwarded-Proto、proxy_redirect并重新规划转发头的传递问题才解决。这个案例给我的启发是代理层级越多越要把协议、Host、转发头当成一等公民在每一层显式传递。5.4 常见问题速查表现象常见原因处理动作重定向次数过多Nginx 与 Java 对协议判断不一致形成死循环统一读 X-Forwarded-Proto配置 forward-headersLocation 出现 127.0.0.1 或内网地址缺少 proxy_set_header Host 或未设置 proxy_redirect补 Host 头用 proxy_redirect 改写前缀旧地址一直跳旧目标浏览器或中间层缓存了 301换 302或引导用户清理缓存跳转后查询参数丢失return 地址没带 $request_uri 或 $args跳转地址显式补充参数前端页面正常但接口 404rewrite 没加 break/last转入其他 location加 last 或改用 return跳转后域名带非预期端口多级代理未正确传递 Host每层显式设置 Host 和 X-Forwarded-Proto最后放一个我自己一直保留的发布习惯每次改重定向前先nginx -T把当前全量配置导出来备份改完必须跑nginx -t然后用不带-L的curl -I把旧地址、短地址、带参数地址分别测一遍确认没有问题之后才让业务方用浏览器验证。这套流程看起来很朴素但每次帮我节省的排障时间都是按小时计算的尤其适合重定向配置比较密集的项目。