0%

微服务网关路由设计:Kong OAuth2 重定向与服务拆分实践

在微服务架构中,API 网关作为流量入口的统一治理层,其路由设计直接影响系统的可维护性和演进能力。记录一次 Kong 网关与 Spring Security OAuth2 集成过程中遇到的重定向问题,以及在服务拆分场景下,通过 Context-Path 配置和 BFF(Backend For Frontend)模式,实现客户端无感知、后端高内聚的微服务演进实践。

一、 问题的产生:散装路由引发的架构困境

1.1 初始场景

我们在本地环境调试 Kong 网关时,发现通过网关访问示例应用(如 http://localhost:18000/java-example),点击”使用企业 IdP 进行登录”按钮后跳转到网关的 /login 路径时,竟然出现了经典的 404 Not Found 错误。

1.2 第一次修复:打补丁的方案

为了快速修复这个错误,我们采用了”打补丁”的方式:在 Kong 配置文件中硬编码了一套散装路由,包含以下路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Kong 中的散装路由配置
routes:
- paths: ["/login"]
service: auth-service
preserve_host: true
strip_path: false
- paths: ["/oauth2"]
service: auth-service
preserve_host: true
strip_path: false
- paths: ["/css", "/js", "/error"]
service: auth-service
preserve_host: true
strip_path: false

结果:虽然登录流程跑通了,但这堆散乱的路由污染了 Kong 网关的核心根目录管理,显得脆弱。在实际运维中,这种散装路由将导致:

  • 维护成本高昂:每个新路径都需要手动配置,容易遗漏和出错。
  • 配置混乱:随着业务增长,路由规则会迅速膨胀,难以管理和理解。
  • 安全风险:权限控制难以统一,容易出现路径穿越或未授权访问。
  • 扩展性差:服务拆分或调整路径时,需要大量修改网关配置,阻碍业务快速迭代。

二、 统一前缀方案:重定向问题分析

既然散装路由太乱,那么能否在 Kong 网关层面把 /login/oauth2 这些零散配置统一套上一层 /auth 前缀进行转发?

答案是否定的,这会引发重定向问题。

2.1 根本原因分析

Spring Security 这类底层认证框架,在构建页面地址或生成向客户端发送的浏览器绝对 Location 重定向响应(即 302 跳转)时,如果它不知道自己实际躲在网关的 /auth 前缀后面,它生成的链接依然会基于根路径 /oauth2/authorization/...

sequenceDiagram
    participant Browser as 浏览器
    participant Kong as Kong 网关
    participant Service as auth-service (Spring Security)

    Browser->>Kong: GET /auth/login
    Kong->>Service: GET /auth/login (preserve_host: false)
    Service-->>Kong: 302 Location: /oauth2/authorization/idp
    Kong-->>Browser: 302 Location: /oauth2/authorization/idp
    Browser->>Kong: GET /oauth2/authorization/idp
    Kong-->>Browser: 404 Not Found (路由不存在)

关键结论:前后端上下文必须强对齐!决不可一边藏头,一边露尾。


三、 Context-Path 与服务拆分的兼容性

3.1 正统解法:服务端配置 Context-Path

为了对齐上下文环境,解法是给 Java 底层的服务直接加上:

1
2
3
4
5
# application.yml
server:
servlet:
context-path: /auth # Context-Path (上下文路径) 定义了 Web 应用程序的根路径。
# 当设置为 /auth 时,所有对该服务的请求都将以 /auth 开头,例如 /auth/login。

3.2 服务拆分后的 API 兼容性

但这就引出了微服务演进史中最直击痛点的问题:如果在代码层写死了服务前缀,未来随着业务变迁导致服务需要切割拆分,客户端的 API URL 是否就得跟着变?

如果客户端需要因为你的后端拆解而被迫全量改代码升级,这网关的防腐层价值岂不是形同虚设?

3.3 网关路由优先级设计

经过深入推导,我们得出了微服务最核心的 API 管理理念:

微服务内部绝不应该关心自己对外的名字叫啥。对外公布的那层 API 契约(如 http://网关/auth/users)全权属于 API 网关的治理资产。

如果我们拆分了部分接口到新服务,客户端 无需改动,仍旧访问旧的 /auth/new-feature

后端开发:在 Kong 中增加一条高优先级的精确前缀劫持路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
routes:
# 通用路由(优先级低)
- name: route-iam
service: auth-service
paths:
- /auth
strip_path: false # 客户端请求 /auth/login,后端服务收到 /auth/login
# Kong 路由匹配原则:通常更长的路径(更具体)具有更高的优先级。

# 精确劫持路由(优先级高)
- name: route-new-feature
service: new-service
paths:
- /auth/new-feature
strip_path: true # 客户端请求 /auth/new-feature/api,后端服务收到 /api
# /auth/new-feature 比 /auth 更长,因此当请求匹配 /auth/new-feature 时,会优先匹配此路由。

配置策略

路由类型 路径配置 strip_path 优先级 说明
通用路由 /auth false 处理大部分 auth-service 的请求
劫持路由 /auth/new-feature true 精确匹配,拦截特定子路径并转发到新服务

用这种组合方式,客户端代码无需修改,通过网关实现服务拆分。


四、 BFF 模式与 OAuth2 安全实践

BFF (Backend For Frontend) 模式,即”服务于前端的后端”,是一种专门为特定前端应用(如 Web、iOS、Android)定制后端 API 的架构模式。它介于传统后端服务和前端之间,负责聚合、转换数据,并处理特定于前端的业务逻辑,以优化前端的用户体验和开发效率。

4.1 纯前端登录方案考量

既然处理带有重定向的 Spring Boot 页面端点比较复杂,我们能否把登录页分离成纯前端微服务(如独立部署的 Vue/React 容器),从而避免后端的重定向绑定逻辑?

4.2 浏览器端 OAuth2 的安全风险

在纯浏览器端完成 OAuth2 闭环获取 Token 并保存(例如 隐式流:直接在浏览器中获取 Access Token;或前端 PKCE:增强授权码流安全性),在当前的安全规范和最佳实践(包括 OAuth 2.1 规范)下,由于易遭受 XSS 跨站攻击 和 Token 窃取,已被业界否定

4.3 BFF 模式的最佳实践

基本原理:OAuth2 授权码交换必须由后端完成,前端不应持有 Token。即发起跟外部 IdP(如企业身份提供商)的握手并换取授权码、Access Token 的操作,应在后端 BFF 层处理(即 auth-service

sequenceDiagram
    participant Browser as 浏览器
    participant BFF as auth-service
    participant IdP as 企业 IdP

    Note right of Browser: 1. 用户点击登录
    Browser->>BFF: GET /auth/login
    BFF-->>Browser: 返回登录页面

    Note right of Browser: 2. 用户点击企业 IdP 登录
    Browser->>BFF: GET /auth/oauth2/authorization/idp
    BFF->>IdP: 302 Location: https://idp.example.com/...

    Note right of Browser: 3. 用户在 IdP 完成认证
    IdP-->>BFF: GET /auth/login/oauth2/code/idp?code=...
    BFF->>IdP: POST /token
    IdP-->>BFF: access_token, refresh_token

    Note right of BFF: 4. Token 存入内存缓存
    BFF-->>Browser: Set-Cookie: SESSION

关键安全措施

  1. BFF 拿到 Token 后装进保险柜(内存缓存)
  2. 外层只给用户的浏览器配发一把 HttpOnly + Secure Cookie 钥匙
  3. Cookie 不应被 JS 读取,防止 XSS 窃取
  4. 前端拿着这把隐形同域 Cookie 钥匙发起一切无感调用

五、 Context-Path 配置方案

5.1 认证 BFF 服务配置

1. auth-service 的 application.yml 增加:

1
2
3
server:
servlet:
context-path: /auth

2. Kong 配置文件清理多余的认证路由(/login 等)。

3. Kong 里恢复原本唯一的那条针对 /auth 路由进行映射,并将透传代理 strip_path 修改为 false

1
2
3
4
5
6
routes:
- name: route-iam
service: auth-service
paths:
- /auth
strip_path: false # 关键:保留 /auth 前缀

4. 示例客户端对准唯一的发起端点进行鉴权握手起步:

1
http://网关:18000/auth/login

5.2 架构延伸:纯 API 微服务是否需要 Context-Path?

如果是针对其他非授权认证页及跳转逻辑的纯净接口微服务(没有绝对重定向概念),它 完全不需要 配置 Context-Path!

直接使用默认在根路径 / 开发,把所有路径的合并组装、修剪去前缀和合并策略,统统在 API 网关用 strip_path: true 完成,保持服务为纯 API 接口节点。

服务类型 是否需要 Context-Path strip_path 原因
认证 BFF false 需要处理重定向,前后端上下文必须对齐
纯 API 微服务 true 无重定向逻辑,网关负责路径修剪,服务保持纯净

六、 总结

在微服务网关路由设计中,核心原则是:找准网关控制面和后台逻辑面的界限,让网关成为真正强大的防腐层

关键决策树

graph TD
    A[微服务路由设计] --> B{服务类型?}
    B -->|认证 BFF| C[配置 Context-Path]
    B -->|纯 API 微服务| D[不配置 Context-Path]
    C --> E[网关 strip_path: false]
    D --> F[网关 strip_path: true]
    E --> G[前后端上下文对齐]
    F --> H[服务保持纯净]
    G --> I[未来拆分?]
    H --> I
    I -->|需要拆分| J[网关精确前缀劫持]
    I -->|暂不拆分| K[维持现状]

核心要点

  1. 前后端上下文必须强对齐:有重定向的服务必须配置 Context-Path,网关不修剪前缀
  2. 网关负责 API 契约:服务内部不关心对外名字,API 契约由网关治理
  3. 服务拆分客户端无感知:通过网关精确前缀劫持,客户端无需因后端拆分而修改代码
  4. BFF 模式处理 OAuth2:授权码交换在后端完成,前端不持有 Token

“在任何架构困境面前,找准网关控制面和后台逻辑面的界限,永远是防腐层发挥威力最大的秘密!”