🌐 语言: 中文版 | English Version
TL;DR
问题: OAuth2 重定向 URL 包含错误端口 http://example.com:9080
根因: APISIX trusted_addresses 配置过窄,导致 X-Forwarded-* 头被覆盖
解决: 将前置代理网段加入 trusted_addresses
问题背景
在一个典型的多层代理架构中:
flowchart TD
Browser[浏览器
HTTPS:443] --> Nginx[Nginx
TLS 终止]
Nginx --> APISIX[APISIX
Docker:9080]
APISIX --> OAuth2[Spring OAuth2
授权服务器]
用户通过 https://public.example.com 访问系统,未登录时访问 OAuth2 授权端点 /oauth2/authorize,理论上应该返回:
1 | 302 Found |
但实际返回的是:
1 | 302 Found |
浏览器访问 https://public.example.com:9080/login 时会报 ERR_CONNECTION_CLOSED,导致用户无法登录。
问题现象分析
浏览器网络请求链路
1 | GET https://public.example.com/oauth2/authorize → 302 |
应用日志关键信息
1 | requestUrl=http://public.example.com:9080/oauth2/authorize |
从日志可以看出,Spring Security 看到的请求视图已经是错误的:
- scheme 被识别成
http - port 被识别成
9080 - SavedRequest 保存的 URL 被污染
排查方向
一开始我们怀疑过几个方向:
- Spring Security 的 SavedRequest 逻辑有问题
- APISIX 的 proxy-rewrite 插件配置不正确
- 应用没有正确开启 forward-headers-strategy
- 入口 Nginx 没把 X-Forwarded-* 头传下来
这些判断都能解释一部分表象,但都不是根本原因。
排查时间线
| 阶段 | 假设 | 验证方法 | 结论 |
|---|---|---|---|
| 1 | Spring Security 问题 | 检查 SavedRequest 逻辑 | ❌ 排除 |
| 2 | proxy-rewrite 配置 | 对比插件配置 | ❌ 排除 |
| 3 | forward-headers-strategy | 检查应用配置 | ❌ 排除 |
| 4 | 前置 Nginx 配置 | 检查 X-Forwarded-* 传递 | ❌ 排除 |
| 5 | APISIX 日志诊断 | 增加诊断日志 | ✅ 定位根因 |
收敛到 APISIX
真正的突破来自给 APISIX 增加诊断日志后,发现:
1 | { |
关键发现:
- 错误的
http/9080已经在 APISIX 里形成了 - 不是应用在返回阶段又”改坏了一次”
- 应用拿到的错误请求视图,本身就是网关传进去的
容易混淆的点
在排查过程中,最容易混淆的是把”用户真实 IP”链路和”公网 origin”链路混在一起。这两件事不是一回事:
用户真实 IP 链路
给风控、审计、限流、日志定位使用,通常依赖:
- X-Real-IP
- X-Forwarded-For
公网 origin 链路
给 OAuth2 重定向、绝对 URL 生成、回调地址判断使用,通常依赖:
- X-Forwarded-Proto
- X-Forwarded-Host
- X-Forwarded-Port
这次问题的本质,不是”用户 IP 没透传”,而是 APISIX 在 trust 判断失败后,把第二条链路改坏了。
APISIX 源码分析
问题最终收敛到 trusted_addresses 配置。
APISIX 在处理请求时的逻辑:
- 计算上一跳地址(通常通过 X-Real-IP 或 Remote Addr)
- 用这个地址去匹配
apisix.trusted_addresses - 如果不可信,就覆盖:
- X-Forwarded-Proto
- X-Forwarded-Host
- X-Forwarded-Port
- 覆盖值不是外部公网入口,而是 APISIX 自己观察到的:
- scheme
- host
- server_port
用伪代码表示:
1 | -- 判断上一跳地址是否在可信列表中 |
这就解释了为什么应用端不断看到 http 和 9080:
- 一旦 APISIX 判定前置代理”不可信”
- 就会把外部 origin 信息降级成自己的监听视图
更多配置细节参考 APISIX 官方文档 - trusted_addresses
解决方案
快速验证
最简单的控制变量实验:
1 | apisix: |
重新加载后,验证 OAuth2 授权端点:
1 | Location: https://public.example.com/login |
问题立即消失。这个实验本身并不是最终配置,但它把根因钉死了:问题就在 APISIX trusted_addresses 这一层。
正式配置
生产环境更合适的做法:
1 | apisix: |
配合前置 Nginx 配置:
1 | location / { |
为什么应用层补丁不是根解
排查过程中,一度在应用层做过几种兜底:
- 自定义 RequestCache,强行把 SavedRequest 改成公网地址
- 自定义登录跳转 URL 生成
- 在过滤器里包装 HttpServletRequest,覆盖 scheme/host/port
这些手段在短期内确实能缓解症状,但不适合作为正式方案:
- 问题不只会出现在一个服务里
- 绝对 URL 的生成不只发生在 OAuth2 登录流程
- 未来 Swagger、邮件链接、前端 API 基地址、回调地址、下载链接都可能踩到同类问题
如果公网访问视图在网关层没有统一治理,最终就会变成每个服务各自打补丁。
开发环境与生产环境的差异
这次问题还有一个现实背景:开发环境和生产环境的网络拓扑并不一致。
开发环境常见的链路:
1 | 宿主机 Nginx → Docker Desktop 端口映射 → APISIX 容器 |
生产环境更常见的是:
1 | Nginx 节点 → 内网 → APISIX 节点 |
两种环境里,APISIX 实际看到的”上一跳地址”可能完全不同。
这会导致一个现象:
- 在开发环境中,用很窄的
trusted_addresses可能根本无法稳定复现生产行为 - 因为 APISIX 拿来做 trust 判断的地址对象,可能已经不是预期中的上一跳代理地址
这也是为什么某些本机环境下,临时把 trusted_addresses 放宽后问题会立刻恢复,而继续猜 CIDR 反而容易陷入死循环。
适用场景
这篇记录适用于类似场景:
- APISIX 部署在反向代理之后
- 对外统一域名和 TLS 终止在前置层
- 下游应用会基于 scheme/host/port 生成绝对 URL
- OAuth2 / OIDC 登录流程对 URL 视图一致性敏感
如果链路中已经出现了下面任一现象:
- 302 Location 带内部端口
- SavedRequest 里出现内网地址或容器端口
- 前端回调或登出地址混入了内网端口
- Mixed Content 指向内网 HTTP 服务
都值得先检查 APISIX 是否覆盖了 X-Forwarded-*。
快速检查清单
遇到类似问题时,按顺序检查:
- APISIX 日志中
server_port是否为内部端口 -
trusted_addresses是否包含前置代理 IP - 前置 Nginx 是否正确设置 X-Forwarded-* 头
- 应用是否开启
forward-headers-strategy - OAuth2 重定向 URL 是否使用内部地址
更多关于 Forwarded Headers 的配置,参考 Spring Security 官方文档。
总结
这个问题本质上不是 OAuth2 协议问题,也不是 Spring Security 的特殊行为。
它更像是一个典型的网关信任边界问题:
- 外部入口是 HTTPS
- 网关内部是 HTTP 9080
- 如果网关没有稳定保留公网访问视图
- 下游服务就会自然把内部端口暴露到绝对 URL 中
而 APISIX 的 trusted_addresses,正是这条链路里的关键开关。
最终问题并不是靠单个”神奇配置”解决的,而是靠一轮完整的收敛:
- 先承认应用补丁不是根解
- 再把问题收敛到网关
- 再用源码和控制变量把根因钉死
- 最后区分开发环境临时恢复方案和生产环境正式配置原则
如果运行链路里同时存在前置代理、网关和需要生成绝对 URL 的应用,这类问题值得优先从 trust 边界而不是业务代码开始看。
这些方案可以帮助你在更复杂的环境中维护 trusted_addresses 的正确性,避免因 IP 变动导致的问题反复出现。