A New Collection of Thoughtful Learning Apps — Now Available on iOS & Android

Image
I’m excited to share a set of mobile apps I’ve recently completed and published on both the Google Play Store and the Apple App Store. These apps are designed with a simple goal in mind: to make meaningful, structured content more accessible, whether you’re studying theology or improving your English vocabulary. 📱 Now Available on Both Platforms All apps are live and available for download: Google Play Developer Page: https://play.google.com/store/apps/dev?id=5835943159853189043 Apple App Store Developer Page: https://apps.apple.com/ca/developer/q-z-l-corp/id1888794100 📖 Theology & Confession Study Apps For those interested in Reformed theology and classical Christian teachings, I’ve developed a series of apps that present foundational texts in a clean, focused reading format: The Belgic Confession Canons of Dort Heidelberg Catechism Westminster Shorter Catechism Each app is designed to provide a distraction-free experience, making it easier to read, reflect, and revisit these im...

一次 403 错误的排查实录:Spring Cloud Gateway、WebFlux、PingFederate 与 CORS

一次 403 错误的排查实录:Spring Cloud Gateway、WebFlux、PingFederate 与 CORS



一次真实的线上排错记录:一个看似 OAuth / JWT / Spring Security 的 403,最终却是 CORS 问题。


背景

我们在一次系统集成过程中,遇到了一个非常令人困惑的问题,涉及以下组件:

  • Spring Cloud Gateway
  • Spring Boot WebFlux 后端服务
  • PingFederate(OIDC / OAuth2)
  • 本地运行的 React 前端(http://localhost:4200

整体请求流程如下:

  1. 用户通过 PingFederate 登录(SSO)
  2. 前端获取 access token
  3. 前端发起 POST 请求 → Gateway → 后端服务

在以下场景中,一切都运行正常:

  • 使用 Postman 直接调用后端
  • 绕过 Gateway,直接调用后端
  • 所有组件都在本地运行

但一旦请求通过 部署在 OpenShift 中的 Spring Cloud Gateway,所有 POST 请求都会失败,并返回:

403 Forbidden

为什么这个问题如此迷惑

  • 完全相同的 access token,在 Postman 中可以正常使用
  • GET 请求成功,POST 请求失败
  • 系统中没有配置任何 role 或 scope 校验
  • 不同环境下 401 / 403 的表现不一致
  • JWT 的 issuer、audience、签名全部正确

所有线索一开始都指向 OAuth 或 PingFederate 的配置问题——但这个判断是错误的。


关键线索:问题是如何被定位出来的

1. Postman 成功,浏览器失败

同一个请求,在不同客户端表现完全不同:

  • Postman → 成功
  • 浏览器(经 Gateway)→ 403

这一步非常关键,它强烈暗示:问题并不在 token 本身


2. 在 Postman 中加上 Origin 头,请求立刻失败

我们在 Postman 中手动加入以下 header:

Origin: http://localhost:4200

请求立刻返回:

403 Forbidden

这一实验直接证实:问题与 CORS 处理 有关。


真正的根因

Spring Security 对 CORS 的默认拦截

后端是一个 Spring Boot WebFlux 应用,并配置为 OAuth2 Resource Server:

oauth2ResourceServer(oauth2 -> oauth2.jwt())

当我们启用 issuer-uri(PingFederate)之后,Spring Security 会启用更严格、也是更正确的安全校验逻辑。当请求中包含 Origin header 时:

  • 请求会被视为 CORS 请求
  • 如果没有显式配置 CORS 策略,Spring Security 会直接拒绝
  • 最终返回 403 Forbidden

这正好解释了所有现象:

  • Postman 成功(默认不带 Origin header)
  • 浏览器 + Gateway 失败(Origin 被转发)
  • GET 通常成功(没有 preflight)
  • POST 失败(触发 CORS 校验)

为什么使用 public-key-location 时没有暴露问题

在旧的配置中,我们使用的是:

jwt:
  public-key-location: classpath:public.key

这种方式下,Spring Security 的行为相对宽松,缺失的 CORS 配置并没有立刻暴露出来。

当切换为:

jwt:
  issuer-uri: https://fed.test.example.com

系统进入了标准 OAuth2 Resource Server 模式,CORS 校验也随之生效


最终的临时解决方案(仅用于本地开发)

在本地调试阶段,我们选择在 Node Gateway 的代理层移除 Origin header:

if (process.env.NODE_ENV === "development" && headers.origin) {
  // 本地调试时避免 Spring Security 的 CORS 拦截
  delete headers.origin;
}

这样可以在不削弱生产环境安全性的前提下,继续本地开发和排错。


推荐的生产环境修复方案

正确、长期的解决方案是:在 WebFlux 后端显式配置 CORS 策略,并且只允许可信来源。

CORS 不应该被绕过,而应该被清晰、明确地管理。


总结与经验教训

  • 403 并不总是权限或角色问题
  • Postman 成功、浏览器失败时,第一时间怀疑 CORS
  • issuer-uri 启用的是更正确、更严格的安全模型
  • Gateway 会转发 Origin header,后端必须明确如何处理

这次问题再次提醒我们:很多“安全问题”,并不出现在单个系统内部, 而是隐藏在系统与系统之间的边界上。

❤️ Support This Blog


If this post helped you, you can support my writing with a small donation. Thank you for reading.


Comments

Popular Posts

Fix “A problem occurred starting process 'command node'” in Android Studio for React Native

Fix up watchman issue with Ghost

Using Mutual TLS (mTLS) in Next.js (Server-Side Only)