LI YAO'S BLOG

有趣的人生, 一半家长里短, 一半山川胡海

深刻理解 `forward-headers-strategy`

深刻理解 forward-headers-strategy

场景复盘(你的案例):

  • 你在项目里用了 Springdoc Open Api + Scalar(接口文档 UI)。
  • 在 Scalar 的 Test Request 功能里发起调用时,页面提示 failed。
  • 打开浏览器控制台/Network 看到请求返回 403。
  • 最终通过在 Spring Boot 中增加配置:
1
2
server:
forward-headers-strategy: framework

解决问题。


1. 它到底解决什么问题?——“反向代理后的真实请求信息”

当你的服务部署在 反向代理 / Ingress / Gateway / LB 后面时,客户端看到的地址通常是:

  • https://api.example.com

但你的 Spring Boot 应用实际收到的连接可能是:

  • 代理到后端用 http
  • Host 变成了内网域名/服务名
  • 端口、scheme、client ip 都不等于用户真实访问的

这会导致一个核心问题:应用层对“当前请求的绝对地址/来源”的判断失真。

而绝对地址、来源判断又被大量框架能力依赖,例如:

  • OpenAPI / Swagger / Scalar 生成的 servers.url(以及 UI 里发请求的目标地址)
  • Spring Security 对 requiresSecure()、重定向地址、登录回调、某些策略判断
  • CORS/CSRF/Origin/Referer 的校验与策略(常见表现之一就是 403)
  • 生成链接(重定向、分页链接、Location header)

代理是怎么把“真实信息”告诉后端的?

常见两套标准:

  • RFC 7239:Forwarded 头
    • 例:Forwarded: proto=https;host=api.example.com;for=1.2.3.4
  • 事实标准:X-Forwarded-*
    • 例:
      • X-Forwarded-Proto: https
      • X-Forwarded-Host: api.example.com
      • X-Forwarded-Port: 443
      • X-Forwarded-For: 1.2.3.4

Spring Boot 是否“信任并使用”这些 header,取决于 **forward-headers-strategy**。


2. forward-headers-strategy 的三个值:none / native / framework

Spring Boot(2.6+ / 3.x)用 server.forward-headers-strategy 来决定:

在什么层面解析并应用 Forwarded / X-Forwarded-*,从而修正 HttpServletRequest 里看到的 scheme/host/port。

2.1 none(默认常见)

  • 不处理 forwarded headers。
  • 应用看到的就是“代理到后端的真实连接信息”(往往是 http、内网 host)。

风险/表现:

  • 文档 UI 可能生成错误地址;
  • 安全策略、重定向、绝对 URL 生成可能异常;
  • 在某些组合下会出现 403(看起来像权限问题,实则是“请求被判定为不合法来源/不安全”。)

2.2 native

  • 依赖 Servlet 容器/服务器本身 来处理 forwarded headers。
  • 典型:Tomcat 的 RemoteIpValve、Undertow/Jetty 的对应机制。

优点:

  • 更靠近网络层,处理更早。

缺点:

  • 对不同容器表现不一致;
  • 在一些组合(例如你引入了 Spring Cloud Gateway、或某些 Filter 顺序/封装请求)下,未必如你预期。

2.3 framework(你最终使用的方案)

  • 由 Spring Framework 在应用层统一处理。
  • 本质是启用/等价于 ForwardedHeaderFilter(或同类机制),将 Forwarded / X-Forwarded-* 应用到当前请求视图。

优点:

  • 行为在 Spring 体系内更一致;
  • 对 “OpenAPI/文档 UI 生成地址” 这类应用层能力更直接;
  • 对你这个 springdoc + Scalar 的场景,通常是最稳的兜底选项。

缺点:

  • 你需要确保:只有在可信代理链路中才会让这些 header 生效(安全问题见下文)。

3. 为什么你的场景会体现为 Scalar Test Request 403?(核心机制)

Scalar 的 Test Request 并不是“随便发个 curl”,它往往依赖 OpenAPI 文档里的 server 信息、当前页面 origin、以及浏览器的同源/CORS 约束,来决定请求要打到哪里。

当 forward-headers-strategy 没开时,Spring Boot 可能把当前请求视为:

  • http://<internal-host>:<port>

于是 springdoc/Scalar 生成或推导出的请求目标地址出现偏差,例如:

  • scheme 从 https 变成 http
  • host 从公网域名变成内网域名
  • port 从 443 变成容器端口

这类“地址失真”会进一步触发:

  • 浏览器侧:跨域限制、混合内容(https 页面去请求 http)、预检失败
  • 服务端侧:Spring Security 认为请求不满足安全约束/来源策略,直接 403

你看到的“403”经常是结果,不是原因。
根因是:应用在代理后没拿到“真实的外部访问视图”,导致文档 UI 发起的请求不符合预期安全策略或路由。

启用 framework 后,Spring 会把 X-Forwarded-Proto=https、X-Forwarded-Host=... 等正确映射回 request.getScheme()/getServerName()/getServerPort(),从而:

  • springdoc 生成的 server url 更接近外部真实地址
  • Scalar Test Request 命中正确的 https + 正确域名
  • 安全策略与路由恢复一致,403 消失

4. 推荐配置:在“有代理”的服务上,优先 framework

4.1 最小可用(你现在的修复)

1
2
server:
forward-headers-strategy: framework

4.2 配套建议(强烈建议你核对)

  1. 确保代理正确设置 forwarded headers

Nginx 示例(仅示意):

1
2
3
4
5
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-Forwarded-For $proxy_add_x_forwarded_for;

Ingress / LB 也要确认是否自动注入 X-Forwarded-* 或 Forwarded。

  1. 只在可信代理链路中使用

forwarded headers 是可伪造的。如果你的应用直接暴露在公网且未经过可信代理,攻击者可以手工构造:

  • X-Forwarded-Proto: https
  • X-Forwarded-Host: evil.com

从而影响你生成的链接、重定向、安全判断等。

因此原则是:

  • 应用只接受来自可信代理的流量(网络层限制)
  • 或在代理层清洗/重写这些 header(只保留代理自己注入的)
  1. 别同时“重复处理”

避免同时开了容器层(native)又开框架层(framework)造成重复修正,出现奇怪的 host/port。


5. 排查清单(下次遇到 403/回调地址异常,可以快速定位)

5.1 先看请求在服务端“被认为是什么”

在某个接口里临时打印(或加一个 debug endpoint):

  • request.getScheme()
  • request.getServerName()
  • request.getServerPort()
  • request.getRequestURL()
  • 以及 headers:Forwarded、X-Forwarded-*

如果你从外部用 https://api.example.com 访问,但服务端看到的是 http://10.x.x.x:8080,那 90% 就是 forwarded headers 没生效。

5.2 再看文档 UI 发请求打到哪里

  • 浏览器 Network 面板里看请求 URL
  • 如果 URL 不是你期望的公网 https 域名,而是内网/HTTP/端口不对,优先修 forwarded headers

6. 结论(给你的落地建议)

  • 你的问题属于典型的“服务在代理后,但应用没有正确恢复外部请求视图”,在 springdoc + Scalar 这种强依赖 server/origin 的 UI 上,最容易表现为 Test Request failed + 403。
  • 在 Spring Boot 体系里,**server.forward-headers-strategy=framework** 通常是最稳定、最一致的选择。
  • 同时要确保:
    • 代理确实注入了正确的 Forwarded/X-Forwarded-*
    • 网络/代理层保证这些 header 不会被客户端伪造利用

附:这个案例的一句话复盘

线上通过反向代理访问服务,Scalar Test Request 返回 403。根因是应用未启用 forwarded headers 解析,导致 Spring 认为请求 scheme/host 为内网 http,从而文档 UI 生成/发起的请求地址与安全策略不一致。设置 server.forward-headers-strategy=framework 后,Spring 正确使用 X-Forwarded-* 恢复外部访问视图,问题解决。

赞赏一杯咖啡