网站明明已经部署,打开却提示ERR_TOO_MANY_REDIRECTS;地址一会儿HTTP、一会儿HTTPS;登录成功后又回到登录页。出现这些现象时,要把每一次跳转写出来,寻找重复经过的地址或条件。
301和302本身不是故障。故障来自多层规则相互冲突,或者登录状态始终无法成立。清除Cookie可以用于比较新旧会话,但如果规则仍有循环,新用户也会再次遇到。
本文先说明如何记录跳转链,再用Nginx终止TLS、Spring Boot处理请求的教学场景定位协议循环,最后排查Cookie与前端登录状态。域名和日志为示例;代理配置仅适用于文中声明的拓扑,不能直接替代完整站点配置。
目录
- 先记录跳转链
- 301、302、307、308别混用
- HTTPS循环经常来自代理协议判断
- 一个简单单入口配置
- 登录页循环要查会话
- 先列出需要验证的入口
- 完整案例:明明访问HTTPS,为什么又跳回同一个地址
- 登录循环:区分"没有保存"与"没有识别"
- 修复前后各留一份跳转链
一、先记录跳转链
浏览器Network启用Preserve log后打开网站,查看每次响应状态与Location。用下面的GET诊断命令也可以查看响应头:
bash
curl -sS -D - -o /dev/null --max-time 10 https://app.example.com/
curl -sS -L --max-redirs 8 --max-time 20 -D - -o /dev/null \
https://app.example.com/

图1:逐步记录跳转目标,区分正常跳转、协议循环、域名循环和登录循环,找到重复出现的节点。
第二条会跟随跳转,但不执行页面里的JavaScript。如果curl正常而浏览器循环,继续查看前端路由守卫、脚本导航和登录状态。某些服务对HEAD与GET行为不同,因此这里使用GET丢弃正文。
二、301、302、307、308别混用
| 状态 | 用途侧重 | 方法处理注意 |
|---|---|---|
| 301 | 永久迁移 | POST可能转为GET |
| 302 | 临时跳转 | POST可能转为GET |
| 307 | 临时跳转 | 保留请求方法和请求体 |
| 308 | 永久跳转 | 保留请求方法和请求体 |
API写请求的跳转尤其要谨慎。不要为了"保留POST"就把所有跳转改成308,还要确认目标地址、凭据范围和重复执行风险。永久跳转可能被缓存,调试时要同时观察新会话和旧会话。

图2:明确哪一层负责规范域名和HTTPS跳转,避免多个入口规则互相纠正而形成循环。
三、HTTPS循环经常来自代理协议判断
典型路径是浏览器用HTTPS访问负载均衡,负载均衡用HTTP访问应用。应用误认为用户还在使用HTTP,于是再次跳到HTTPS;用户重新到达负载均衡,又重复同一过程。
解决方向是明确在哪一层终止TLS、在哪一层执行规范化跳转,以及应用如何识别原始协议。可信代理可以传递Forwarded或X-Forwarded-Proto,应用需要按部署模型配置处理。

图3:TLS在代理终止后,应用需要通过可信代理信息识别原始协议,不能直接相信客户端转发头。
不能直接相信任何客户端提交的转发头。入口应按真实连接重写这些字段,并限制应用只接受可信代理流量。若前面还有CDN,Nginx的$scheme可能只是CDN到源站的协议,需要按完整链路判断,不能照搬单层代理样例。
四、一个简单单入口配置
下面只展示HTTP重定向入口,假设443站点已有有效TLS配置,且规范域名固定为app.example.com:
nginx
server {
listen 80;
server_name app.example.com;
return 301 https://app.example.com$request_uri;
}
固定规范域名可以避免把任意Host原样拼到重定向目标。还应检查其他站点块、CDN规则和应用是否配置了相反方向的跳转。www与非www也应只有一个明确最终目标。
每次改动先语法检查、再发布、再抓取跳转链。不要同时改三个代理层,否则恢复后难以判断具体冲突。
五、登录页循环要查会话
登录接口成功后,浏览器是否真正保存了Cookie?跳到目标页面时是否携带它?Cookie的Domain、Path、Secure和SameSite是否适配当前入口?这些比"延迟两秒再跳转"更值得先查。

图4:从登录结果、Cookie范围、后续请求到前端路由守卫依次检查,定位登录状态在哪一步丢失。
前端路由守卫还可能把登录页本身也当作受保护页面,或者在用户信息尚未初始化时立即重定向。应明确匿名页面、初始化中和已认证三种状态,给失败设置终止路径。
多实例会话不共享时,登录在A实例成功、业务请求到B实例失效,也会表现为循环。要结合实例标识和会话策略诊断,不能只要求用户清缓存。
六、先列出需要验证的入口

图5:验证最终地址、跳转次数、登录状态和请求方法,避免页面能打开但写操作行为被跳转改变。
| 场景 | 预期 |
|---|---|
| HTTP首页 | 有限跳转到规范HTTPS地址 |
| HTTPS深层路径 | 保留路径与必要参数 |
| 未登录受保护页 | 到登录页并能停止 |
| 登录后返回 | 能回到允许的目标页面 |
| 失效会话 | 清晰提示,无无限循环 |
登录后的返回地址需要校验为受信任站内目标,避免开放重定向。测试时既用新会话,也用已有登录状态,并检查移动端入口。
接下来用具体响应头和代理配置,找出"谁把请求再次送回原地址"。
七、完整案例:明明访问HTTPS,为什么又跳回同一个地址
下面用一个常见拓扑说明问题:浏览器通过HTTPS访问Nginx,Nginx终止TLS,再用HTTP转发到Spring Boot。如果应用只看到内部HTTP连接,又要求用户切换HTTPS,就可能不断返回同一个公网HTTPS地址。
这是教学场景,不代表你的系统必然使用这种架构。前方存在CDN、负载均衡或Ingress时,应把每一层都画出来。
1. 先证明循环由HTTP响应产生
在受控终端访问无副作用的页面,先不要携带真实Cookie:
bash
curl -sS -D - -o /dev/null --max-time 10 \
https://app.example.com/projects
curl -sS -L --max-redirs 5 --max-time 20 -D - -o /dev/null \
https://app.example.com/projects
假设看到类似结果:
text
HTTP/1.1 302
Location: https://app.example.com/projects
HTTP/1.1 302
Location: https://app.example.com/projects
相同URL反复出现,说明服务器没有因为客户端已经使用HTTPS而改变判断。但这只证明循环存在,还不能证明一定来自Spring Boot;需要查看代理与应用日志。
如果命令返回200、浏览器却仍在跳转,curl不会执行JavaScript,接下来应检查前端导航、页面刷新和登录守卫,而不是继续只调Nginx。
2. 分别记录代理输出与上游输出的Location
以下格式声明放在http上下文,再配置到目标站点的access_log:
nginx
log_format redirects '$time_iso8601 rid=$request_id '
'scheme=$scheme status=$status '
'upstream=$upstream_status '
'sent_location="$sent_http_location" '
'upstream_location="$upstream_http_location"';
这是临时诊断格式。Location可能含授权码、返回地址或其他敏感参数,访问权限、保留时间和脱敏必须受控,不能直接公开日志。
若上游本来就返回302及循环目标,继续查应用判断;若上游没有对应跳转,而出口出现301/302,查看入口return、rewrite或响应头改写。字段缺失也可能受内部跳转、缓存和多次上游尝试影响,应结合生效配置确认。
3. 单层TLS终止时,明确传递原始协议
下面片段只适用于"客户端直接到此Nginx,由此Nginx终止TLS,后台不允许公网直连"的模型,放入已有443站点对应location:
nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host app.example.com;
proxy_set_header X-Forwarded-Host app.example.com;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port 443;
# 此示例使用X-Forwarded-*,移除客户端自带Forwarded
proxy_set_header Forwarded "";
}
项目中其他转发头同样要检查可信来源,不能只处理这一项就宣称所有代理头都安全。
Spring Boot可以根据部署方式选择转发头处理策略。以下是使用框架处理的配置示意,实际版本和既有过滤器应先核对:
yaml
server:
forward-headers-strategy: framework
不要再无条件叠加第二个ForwardedHeaderFilter,避免重复处理。打开这项配置的前提是入口对转发头进行清理或覆盖,并限制客户端不能绕过入口直连应用,否则伪造的协议与主机信息可能影响生成链接。
如果Nginx前面还有CDN并通过HTTP回源,此时$scheme是回源协议,不是浏览器协议,上面的片段就不能直接照搬。需要按可信代理链配置,或者统一采用匹配架构的HTTPS回源方案。
八、登录循环:区分"没有保存"与"没有识别"
以Cookie会话为例,按下面顺序核对:
| 环节 | 具体证据 | 可能问题 |
|---|---|---|
| 登录响应 | 是否有Set-Cookie,浏览器是否提示拒绝 | Cookie属性不符合当前环境 |
| 浏览器存储 | 目标域名下是否实际保存 | Domain、Path或过期条件 |
| 业务请求 | 是否携带对应Cookie | 范围、Secure或跨站限制 |
| 服务端读取 | 会话是否存在并有效 | 多实例存储或会话过期 |
| 前端判断 | 是否完成用户信息初始化 | 过早执行路由守卫 |
HttpOnly Cookie不能由JavaScript读取是正常保护行为,不能因此把用户当成未登录;也不应通过去掉HttpOnly修复状态判断。
假设登录在A实例创建本机会话,下一次请求到B实例时找不到会话,会出现"偶尔登录成功,刷新又掉线"。需要结合实例标识验证,再决定共享会话或其他一致性方案,不能仅靠清Cookie解决。
路由守卫的关键是状态,而不是延迟
前端至少区分未知、加载中、已登录和未登录。可以在导航前等待一次受控的会话恢复,再判断是否重定向,而不是每个请求失败都跳到登录页。
以下为流程伪代码,不是可直接粘贴的Vue Router实现:
text
访问匿名页面:
允许进入,不再次要求登录
访问受保护页面:
若会话未知,等待同一个会话恢复任务
若确认已登录,继续进入
若确认未登录,进入登录页
若恢复接口网络失败,显示错误与重试入口
网络失败不等于未登录;混为一谈,可能让短暂故障演变成登录跳转循环。登录后的返回地址优先采用站内路由白名单,不直接信任任意returnUrl。
九、修复前后各留一份跳转链
只修改一组已确认冲突的规则,先通过nginx -t再按既有流程重载。若语法检查失败,不应继续重载。
修复后不仅检查首页,还要记录:
| 测试入口 | 合格表现 |
|---|---|
| HTTP首页 | 到达规范HTTPS地址并终止 |
| HTTPS深层路径 | 不重复自跳转,路径正确 |
| 带参数页面 | 必要参数保留,敏感参数不泄露 |
| 未登录受保护页 | 到登录页后停止 |
| 登录成功 | 进入允许的站内目标 |
| 会话恢复失败 | 显示明确错误,不无限导航 |
| API写请求 | 方法、目标和认证符合接口设计 |
不要用真实写接口测试重定向方法变化。需要比较301/302与307/308时,在独立测试环境用回显请求方法的无副作用接口验证。
永久跳转缓存会影响旧浏览器表现。保留新会话、旧会话和curl三组结果,有助于区分"规则没有修好"与"客户端仍在使用历史跳转"。