0%

APISIX 网关层 OAuth2 重定向端口问题排查

🌐 语言: 中文版 | 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
2
HTTP/1.1 302 Found
Location: https://public.example.com/login

但实际返回的是:

1
2
HTTP/1.1 302 Found
Location: http://public.example.com:9080/login

浏览器访问 https://public.example.com:9080/login 时会报 ERR_CONNECTION_CLOSED,导致用户无法登录。

问题现象分析

浏览器网络请求链路

1
2
3
GET https://public.example.com/oauth2/authorize → 302
GET http://public.example.com:9080/login → 307 Internal Redirect
GET https://public.example.com:9080/login → FAILED

应用日志关键信息

1
2
3
4
5
requestUrl=http://public.example.com:9080/oauth2/authorize
scheme=http serverName=public.example.com serverPort=9080
Saved request http://public.example.com:9080/oauth2/authorize?...
Redirecting to /login
location=http://public.example.com:9080/login

从日志可以看出,Spring Security 看到的请求视图已经是错误的:

  • scheme 被识别成 http
  • port 被识别成 9080
  • SavedRequest 保存的 URL 被污染

排查方向

一开始我们怀疑过几个方向:

  1. Spring Security 的 SavedRequest 逻辑有问题
  2. APISIX 的 proxy-rewrite 插件配置不正确
  3. 应用没有正确开启 forward-headers-strategy
  4. 入口 Nginx 没把 X-Forwarded-* 头传下来

这些判断都能解释一部分表象,但都不是根本原因。

排查时间线

阶段 假设 验证方法 结论
1 Spring Security 问题 检查 SavedRequest 逻辑 ❌ 排除
2 proxy-rewrite 配置 对比插件配置 ❌ 排除
3 forward-headers-strategy 检查应用配置 ❌ 排除
4 前置 Nginx 配置 检查 X-Forwarded-* 传递 ❌ 排除
5 APISIX 日志诊断 增加诊断日志 ✅ 定位根因

收敛到 APISIX

真正的突破来自给 APISIX 增加诊断日志后,发现:

1
2
3
4
5
6
7
8
9
{
"host": "public.example.com",
"server_port": "9080",
"http_x_forwarded_host": "public.example.com",
"http_x_forwarded_proto": "http",
"http_x_forwarded_port": "9080",
"sent_http_location": "http://public.example.com:9080/login",
"upstream_http_location": "http://public.example.com:9080/login"
}

关键发现:

  • 错误的 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 在处理请求时的逻辑:

  1. 计算上一跳地址(通常通过 X-Real-IP 或 Remote Addr)
  2. 用这个地址去匹配 apisix.trusted_addresses
  3. 如果不可信,就覆盖:
    • X-Forwarded-Proto
    • X-Forwarded-Host
    • X-Forwarded-Port
  4. 覆盖值不是外部公网入口,而是 APISIX 自己观察到的:
    • scheme
    • host
    • server_port

用伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 判断上一跳地址是否在可信列表中
local addr_is_trusted = trusted_addresses_util.is_trusted(api_ctx.var.realip_remote_addr)

if not addr_is_trusted then
-- 不可信时,用网关自己的视图覆盖 X-Forwarded-* 头
local proto = api_ctx.var.scheme -- 实际值:http
local host = api_ctx.var.host -- 实际值:public.example.com
local port = api_ctx.var.server_port -- 实际值:9080

core.request.set_header(api_ctx, "X-Forwarded-Proto", proto)
core.request.set_header(api_ctx, "X-Forwarded-Host", host)
core.request.set_header(api_ctx, "X-Forwarded-Port", port)
end

这就解释了为什么应用端不断看到 http9080

  • 一旦 APISIX 判定前置代理”不可信”
  • 就会把外部 origin 信息降级成自己的监听视图

更多配置细节参考 APISIX 官方文档 - trusted_addresses

解决方案

快速验证

最简单的控制变量实验:

1
2
3
4
apisix:
trusted_addresses:
- 0.0.0.0/0
- ::/0

重新加载后,验证 OAuth2 授权端点:

1
Location: https://public.example.com/login

问题立即消失。这个实验本身并不是最终配置,但它把根因钉死了:问题就在 APISIX trusted_addresses 这一层。

正式配置

生产环境更合适的做法:

1
2
3
4
5
6
7
8
apisix:
node_listen: 9080
enable_ipv6: false
trusted_addresses:
- 127.0.0.1 # 本地回环
- "::1" # IPv6 本地回环
- 172.17.21.0/24 # Docker 网络(如 Docker Desktop)
- 192.168.10.0/24 # 内网网络(前置代理所在网络)

配合前置 Nginx 配置:

1
2
3
4
5
6
7
8
9
10
location / {
proxy_pass http://apisix;

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

为什么应用层补丁不是根解

排查过程中,一度在应用层做过几种兜底:

  • 自定义 RequestCache,强行把 SavedRequest 改成公网地址
  • 自定义登录跳转 URL 生成
  • 在过滤器里包装 HttpServletRequest,覆盖 scheme/host/port

这些手段在短期内确实能缓解症状,但不适合作为正式方案:

  1. 问题不只会出现在一个服务里
  2. 绝对 URL 的生成不只发生在 OAuth2 登录流程
  3. 未来 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 变动导致的问题反复出现。