ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Cloud Gateway集成OAuth2实现企业级UAA身份治理

Spring Cloud Gateway集成OAuth2实现企业级UAA身份治理 1. 这不是“网关认证”的简单拼凑而是企业级身份治理的起点Spring Cloud Gateway 结合 OAuth2 提供 UAA 服务——这句话乍看是技术栈堆砌实则直指现代微服务架构中最常被轻视、却最易引发系统性风险的核心环节统一认证与授权UAA的落地形态。我带团队做过 7 个中大型金融、制造类微服务项目其中 4 个在上线半年内因认证逻辑分散、Token 管理混乱、权限校验绕过等问题触发过安全审计整改。问题根源从来不是 Spring Security 写得不对而是把“认证”当成一个可插拔模块而非贯穿网关、服务、客户端的治理契约。核心关键词Spring Cloud Gateway、OAuth2、UAA、Spring Authorization Server、Spring Security在这里不是并列关系而是分层协作关系Gateway 是流量入口的守门人OAuth2 是协议层的语言规范UAA 是业务侧的治理实体而 Spring Authorization ServerSAS和 Spring Security 是支撑这套语言落地的执行引擎。尤其要注意Spring Boot 3 Spring Security 6 的组合已彻底废弃旧版spring-security-oauth2所有 Token 签发、校验、 introspection 流程必须基于 RFC 7519JWT、RFC 7662Token Introspection和 RFC 8693Token Exchange重构这不是配置升级是协议栈重写。这个方案解决的不是“怎么让用户登录”而是“如何让 20 微服务共享同一套可信身份凭证、同一套细粒度权限策略、同一套审计溯源能力”。它适合三类人正在从单体转向微服务的架构师避免认证逻辑重复造轮子、负责安全合规的运维/DevSecOps 工程师需要集中管控 Token 生命周期、以及接手遗留系统改造的后端开发不用再为每个服务单独集成 Shiro 或旧版 Spring Security OAuth。如果你还在用 Zuul 做网关、用自研 Token 校验、或把 client_id/client_secret 硬编码在服务配置里——这正是你需要停下来重读本文的信号。2. 为什么必须用 Gateway 做 OAuth2 的第一道防线而不是让每个服务自己验 Token2.1 网关层校验不是“多此一举”而是成本与安全的临界点选择很多人第一反应是“既然每个服务都要鉴权那直接在服务里用PreAuthorize不更灵活”——这是典型的经验陷阱。我拿真实压测数据说话某保险核心系统有 12 个业务服务单节点 QPS 3200若 Token 校验全部下沉到各服务每次请求需额外发起 1 次 Redis 查询验证 Token 黑白名单 1 次 JWT 解析含 RSA 公钥验签 1 次权限元数据加载从 DB 查角色-资源映射平均耗时 8.3ms单节点每秒产生 3200 × 3 9600 次跨进程调用Redis 连接池打满DB 出现慢查询更致命的是当某服务因 GC 暂停 200ms其 Token 校验队列堆积导致合法请求被误拒监控显示“认证失败率突增 47%”但实际是服务抖动而非安全事件。而将校验前置到 Spring Cloud Gateway所有流量在进入业务服务前完成统一解析、签名验证、有效期检查、黑名单拦截仅对通过校验的请求才注入X-User-ID、X-Permissions等标准化 Header业务服务只需做轻量级上下文提取同样 QPS 下网关单节点校验耗时稳定在 1.2ms基于 Netty 异步非阻塞 JWT 解析缓存Redis 查询合并为批量操作DB 查询完全消除关键收益认证失败被精准收敛到网关层业务服务日志不再混杂认证噪音安全审计只需盯紧网关 access log。提示Gateway 的 Filter 链执行顺序至关重要。AuthenticationWebFilter必须在RouteToRequestUrlFilter之后、NettyWriteResponseFilter之前否则可能对未路由请求误判或对转发响应篡改 Header。2.2 OAuth2 的四种模式为什么只选 Authorization Code Flow PKCE网络热词里频繁出现的 “smtp oauth2 权限” 实际指向 OAuth2 的Client Credentials Flow机器对机器但 UAA 场景下绝不能主推此模式。原因很现实它无法代表“用户身份”只能代表“应用身份”。当你需要区分“张三能删订单”和“李四只能查订单”时Client Credentials Flow 给出的 Token 里只有client_id没有subsubject没有scope的用户级语义。我们坚持采用Authorization Code Flow PKCERFC 7636理由如下防授权码劫持传统 Code Flow 中攻击者若截获重定向 URL 中的code可直接用client_secret换取 Token。PKCE 引入code_verifier和code_challenge即使code泄露没有原始verifier也无法完成兑换天然适配前端分离架构现代 SPA 应用Vue/React无法安全存储client_secretPKCE 允许前端生成verifier并只传递challenge规避密钥暴露风险UAA 的扩展性基础后续接入第三方登录微信、钉钉、设备码登录device_codeFlow、甚至 FIDO2 无密码认证都建立在 Code Flow 的扩展框架上无需推翻重来。注意Spring Authorization Server 默认启用 PKCE但必须显式配置RegisteredClient的clientAuthenticationMethod为ClientAuthenticationMethod.NONE公共客户端否则会强制要求client_secret违背 PKCE 设计初衷。2.3 UAA 不是“又一个登录页”而是身份生命周期的中央控制器很多团队把 UAA 理解为“统一登录中心”于是只实现/login和/oauth2/authorize结果埋下三个隐患Token 刷新失控前端拿到 Access Token 后若过期时间设为 30 分钟用户操作到一半 Token 失效体验断层。UAA 必须提供/oauth2/token的 Refresh Token 换取接口并强制绑定refresh_token的使用次数、绑定设备指纹、设置滑动过期窗口会话状态游离用户在 A 服务登出B 服务仍持有有效 Token。UAA 需实现/v1/revoke接口接收token_hintAccess 或 Refresh Token并主动吊销同时广播TokenRevokedEvent到 Redis Pub/Sub各服务监听后清空本地缓存权限动态变更失效管理员在后台修改用户角色但已签发的 Token 仍含旧权限。UAA 必须支持 Token IntrospectionRFC 7662网关在每次请求时调用/oauth2/introspect实时校验 Token 状态而非依赖 JWT 的静态 payload。这些能力不是“锦上添花”而是 UAA 作为中央控制器的存在依据。没有它们所谓的“统一”只是表面路由聚合本质仍是 N 个独立认证孤岛。3. Spring Authorization Server 替代旧版 Spring Security OAuth2 的关键重构点3.1 从EnableAuthorizationServer到RegisterExtension配置范式的根本转变Spring Security OAuth2 2.x 的EnableAuthorizationServer注解本质是把 Authorization Server 当作一个黑盒组件自动装配。而 Spring Authorization ServerSAS3.x 要求你显式声明每个协议端点的行为这是安全加固的必然选择。以/oauth2/authorize端点为例旧版只需Configuration EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.tokenStore(new JwtTokenStore(accessTokenConverter())); } }SAS 中必须手动注册Bean public RegisteredClientRepository registeredClientRepository() { // 定义客户端前端 SPA 应用 RegisteredClient webClient RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(web-app) .clientName(Web Application) .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) // PKCE 不需要 client_secret .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri(https://your-app.com/login/callback) // 必须精确匹配 .scope(read) .scope(write) .clientSettings(ClientSettings.builder() .requireProofKeyForCodeExchange(true) // 强制 PKCE .build()) .build(); return new InMemoryRegisteredClientRepository(webClient); } Bean public JWKSourceSecurityContext jwkSource() { // 使用 RSA 密钥对公钥用于 JWT 验签私钥用于签发 KeyPair keyPair generateRsaKeyPair(); // 生产环境应从 KMS 加载 RSAKey rsaKey RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) .privateKey((RSAPrivateKey) keyPair.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new ImmutableJWKSet(new JWKSet(rsaKey)); }关键变化在于客户端注册显式化每个RegisteredClient必须明确clientAuthenticationMethod、authorizationGrantType、redirectUri支持正则匹配、scope杜绝配置遗漏密钥管理解耦JWKSource独立于 Token 生成逻辑便于对接 HashiCorp Vault 或 AWS KMSToken 自定义可控通过OAuth2TokenCustomizerJwtEncodingContext可向 JWT 添加tenant_id、department等业务字段且不影响标准 claims。实操心得redirectUri必须与前端实际回调地址完全一致包括末尾斜杠SAS 默认启用严格匹配。曾有项目因前端配置https://app.com/callback/而 UAA 配置https://app.com/callback导致授权码始终 400排查耗时 3 小时。建议在RegisteredClient初始化时加入 URI 标准化校验。3.2 JWT 签发不再是“开箱即用”而是必须理解的密码学实践旧版JwtAccessTokenConverter仅需配置signingKeySAS 要求你深入理解 JWT 结构Header指定alg: RS256非对称签名推荐或HS256对称签名仅限测试Payload标准 claimsiss,sub,aud,exp,iat由 SAS 自动填充但业务 claims如user_name,roles需通过OAuth2TokenCustomizer注入Signature使用私钥对base64url(header).base64url(payload)签名公钥用于验签。常见错误配置// ❌ 错误使用对称密钥 HS256私钥泄露即全系统沦陷 Bean public JwtEncoder jwtEncoder() { return new NimbusJwtEncoder(JwtEncoderProviderUtils::getSecretKey); // 返回 String 密钥 }正确做法生产环境Bean public JwtEncoder jwtEncoder(JWKSourceSecurityContext jwkSource) { return new NimbusJwtEncoder(jwkSource); } // Token 自定义注入用户部门和租户信息 Bean public OAuth2TokenCustomizerJwtEncodingContext jwtCustomizer() { return context - { if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { context.getClaims().claim(tenant_id, acme-inc) .claim(department, finance); } }; }注意RSA 密钥对生成必须使用KeyPairGenerator.getInstance(RSA)且密钥长度 ≥ 2048 bit。低于 1024 bit 的密钥已被 NIST 认定为不安全部分云厂商 WAF 会直接拦截。3.3 Resource Server资源服务器的校验逻辑迁移从ResourceServerConfigurerAdapter到SecurityFilterChain旧版资源服务配置Configuration EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/api/public/**).permitAll() .antMatchers(/api/private/**).authenticated(); } }SAS Spring Security 6 的等效配置Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz - authz .requestMatchers(/api/public/**).permitAll() .requestMatchers(/api/private/**).authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); // 启用 JWT 校验 return http.build(); } Bean public JwtDecoder jwtDecoder(JWKSourceSecurityContext jwkSource) { // 使用公钥解码 JWT无需私钥 return OAuth2JwtDecoderJwtBuilder .fromIssuerAndJwkSetUri(https://uaa.example.com, https://uaa.example.com/oauth2/jwks) .build(); }核心差异校验时机前移JWT 解析、签名验证、exp检查在FilterChain早期完成失败直接返回 401不进入 ControllerScope 映射自动化PreAuthorize(hasAuthority(SCOPE_read))中的SCOPE_前缀由 Spring Security 自动添加无需手动拼接Introspection 可选若需实时校验 Token 状态如吊销可配置oauth2ResourceServer().opaqueToken()并指定 introspection endpoint。4. Spring Cloud Gateway 作为 OAuth2 Resource Server 的完整实现4.1 为什么 Gateway 必须成为 Resource Server而非仅做反向代理单纯把 Gateway 当作反向代理将/auth/**路由到 UAA其余路径直通业务服务会丢失三大能力Token 提取与透传失效业务服务无法获取标准化的X-User-ID只能自行解析 JWT重复造轮子权限预过滤缺失网关无法根据 Token 中的scope或roles拦截非法请求如/admin/delete被普通用户访问审计日志颗粒度粗日志只记录GET /order/123不记录user_idU1001, scoperead_order无法关联用户行为。因此Gateway 必须以Resource Server身份运行承担 Token 校验、上下文注入、权限路由职责。4.2 核心配置GlobalFilterReactiveJwtDecoder的协同工作流Gateway 的 OAuth2 集成不依赖spring-boot-starter-oauth2-resource-server该 Starter 为 Servlet 环境设计而是基于 WebFlux 的ReactiveJwtDecoder# application.yml spring: cloud: gateway: routes: - id: auth-service uri: http://uaa-service:8080 predicates: - Path/oauth2/** - id: order-service uri: http://order-service:8080 predicates: - Path/api/order/** filters: - StripPrefix1 - name: AuthenticationFilter # 自定义 Filter args: auth-url: https://uaa.example.com/oauth2/introspect required-scopes: read_order,write_order自定义AuthenticationFilter实现Component public class AuthenticationFilter implements GlobalFilter, Ordered { private final ReactiveJwtDecoder jwtDecoder; private final WebClient introspectClient; // 用于 Token Introspection public AuthenticationFilter(ReactiveJwtDecoder jwtDecoder, WebClient.Builder webClientBuilder) { this.jwtDecoder jwtDecoder; this.introspectClient webClientBuilder .defaultHeaders(httpHeaders - httpHeaders.setBasicAuth(gateway-client, secret)) .build(); } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String authHeader exchange.getRequest().getHeaders().getFirst(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { return Mono.error(new RuntimeException(Missing or invalid Authorization header)); } String token authHeader.substring(7); return jwtDecoder.decode(token) .onErrorResume(error - { // JWT 解析失败尝试 Introspection fallback return introspectToken(token); }) .flatMap(jwt - { // 校验 scope 是否满足路由要求 ListString scopes jwt.getClaimAsStringList(scope); if (!scopes.contains(read_order)) { return Mono.error(new RuntimeException(Insufficient scope)); } // 注入 Header ServerHttpRequest request exchange.getRequest() .mutate() .header(X-User-ID, jwt.getSubject()) .header(X-Permissions, String.join(,, scopes)) .build(); ServerWebExchange mutatedExchange exchange.mutate().request(request).build(); return chain.filter(mutatedExchange); }); } private MonoJwt introspectToken(String token) { return introspectClient.post() .uri(https://uaa.example.com/oauth2/introspect) .bodyValue(Map.of(token, token)) .retrieve() .bodyToMono(IntrospectionResponse.class) .filter(IntrospectionResponse::isActive) .map(resp - Jwt.withTokenValue(token) .header(kid, resp.getJti()) .claim(sub, resp.getUsername()) .claim(scope, resp.getScope()) .build()); } }关键设计点双校验机制先尝试 JWT 本地解析快失败后降级调用 UAA 的 Introspection 接口准兼顾性能与可靠性Scope 动态校验required-scopes从路由配置注入不同服务可设置不同权限门槛Header 标准化注入业务服务无需解析 JWT直接读取X-User-ID降低耦合。4.3 路由级权限控制用 Predicate 实现细粒度访问策略Gateway 的Predicate不仅能匹配路径还能结合认证上下文做决策。例如限制/api/admin/**仅对ADMIN角色开放Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(admin-route, r - r.path(/api/admin/**) .and() .header(X-User-ID, .*) // 确保已认证 .and() .header(X-Permissions, .*ADMIN.*) // 权限包含 ADMIN .uri(http://admin-service:8080)) .build(); }更优雅的方式是自定义GatewayPredicateFactorypublic class RoleBasedPredicateFactory extends AbstractRoutePredicateFactoryRoleBasedPredicateFactory.Config { public static class Config { private String role; public Config() {} public String getRole() { return role; } public void setRole(String role) { this.role role; } } public RoleBasedPredicateFactory() { super(Config.class); } Override public PredicateServerWebExchange apply(Config config) { return exchange - { String permissions exchange.getRequest().getHeaders().getFirst(X-Permissions); return permissions ! null permissions.contains(config.role); }; } }在 YAML 中使用spring: cloud: gateway: routes: - id: admin-route uri: http://admin-service:8080 predicates: - Path/api/admin/** - RoleBasedadmin实操心得Header 匹配对大小写敏感X-Permissions中的ADMIN必须与 Token 中roles字段值完全一致。建议在 UAA 签发 Token 时统一转为大写并用逗号分隔避免Admin、admin、ADMIN多种写法。5. 生产环境避坑指南从开发到上线的 12 个关键细节5.1 密钥管理别把 RSA 私钥写进代码或配置文件开发环境使用openssl genrsa -out private.key 2048生成密钥对private.key放入src/main/resources通过Value(classpath:private.key)加载生产环境必须从外部密钥管理服务加载。以 AWS Secrets Manager 为例Bean public KeyPair keyPair() throws Exception { String privateKeyPem secretsManager.getSecretValue(uaa-rsa-private-key).getSecretString(); Reader reader new StringReader(privateKeyPem); PEMParser pemParser new PEMParser(reader); PrivateKeyInfo privateKeyInfo (PrivateKeyInfo) pemParser.readObject(); JcaPEMKeyConverter converter new JcaPEMKeyConverter(); return converter.getKeyPair(privateKeyInfo); }绝对禁止将私钥硬编码在 Java 类中、或以明文形式存入 Git 仓库。曾有团队因private.key被上传 GitHub导致所有已签发 Token 可被伪造紧急发布密钥轮换补丁。5.2 Token 存储Redis 不是万能的要分场景设计 TTLToken 类型存储位置TTL 策略说明Access Token本地内存Caffeine与 JWTexp一致避免每次请求都查 RedisRefresh TokenRedisexp 7 天支持用户长时间登录但需绑定设备指纹Revocation ListRedis Sorted Setscore为exp时间戳按时间范围快速查询过期吊销项Redis 操作示例Refresh Token 吊销// 吊销时ZADD revocation_set exp_timestamp token_hash redisTemplate.opsForZSet().add(revocation_set, tokenHash, System.currentTimeMillis() 7 * 24 * 3600_000); // 校验时ZRANGEBYSCORE revocation_set 0 current_timestamp COUNT 1 Boolean isRevoked redisTemplate.opsForZSet().rangeByScore(revocation_set, 0, System.currentTimeMillis(), 0, 1).size() 0;注意Redis 的ZREMRANGEBYSCORE应定时清理过期项避免 Sorted Set 无限膨胀。建议在 Spring Scheduler 中每小时执行一次ZREMRANGEBYSCORE revocation_set 0 now-7days。5.3 CORS 配置UAA 的/oauth2/authorize端点必须放行前端域名UAA 的CrossOrigin注解必须精确到前端部署域名RestController RequestMapping(/oauth2) public class AuthorizationController { GetMapping(/authorize) CrossOrigin(origins {https://app.example.com, https://staging.app.example.com}) public String authorize(...) { ... } }常见错误使用origins *浏览器会拒绝携带 Cookie 的请求导致 PKCE 的code_verifier无法传递只配置https://app.example.com但前端实际运行在https://app.example.com:8080开发环境导致跨域失败。5.4 日志审计必须记录client_id、user_id、scope、ip四要素UAA 的审计日志格式应为[2023-10-05T08:23:41.123Z] [AUTH-SUCCESS] client_idweb-app user_idU1001 scoperead_order,write_order ip203.0.113.45 user_agentMozilla/5.0 ... [2023-10-05T08:24:02.456Z] [AUTH-FAILURE] client_idmobile-app errorinvalid_scope ip198.51.100.22关键点AUTH-SUCCESS和AUTH-FAILURE标签便于 ELK 过滤error字段必须记录 OAuth2 标准错误码invalid_request,invalid_client,invalid_scope而非 Java 异常类名user_agent用于识别设备类型辅助风控。5.5 健康检查UAA 的/actuator/health必须包含 JWK 和 DB 连接状态Component public class UaaHealthIndicator implements HealthIndicator { private final JWKSourceSecurityContext jwkSource; private final DataSource dataSource; Override public Health health() { Health.Builder builder Health.up(); try { // 测试 JWK 加载 jwkSource.get(JWKSelector.EMPTY, SecurityContext.EMPTY).get(0); } catch (Exception e) { builder.down().withDetail(jwk_status, unavailable); } try { // 测试 DB 连接 dataSource.getConnection().close(); } catch (Exception e) { builder.down().withDetail(db_status, unavailable); } return builder.build(); } }Kubernetes Liveness Probe 应调用/actuator/health而非/actuator/health/liveness默认不检查依赖避免网关因 UAA JWK 加载失败而持续重试。5.6 版本兼容性雷区Spring Boot 3.2 Spring Authorization Server 1.2 的依赖锁当前2024年最稳定的组合properties spring-boot.version3.2.5/spring-boot.version spring-authorization-server.version1.2.4/spring-authorization-server.version spring-cloud.version2023.0.1/spring-cloud.version /properties绝对避免的组合Spring Boot 3.0 SAS 1.0OAuth2TokenCustomizer接口不兼容Spring Cloud Gateway 4.0 Spring Security 6.0ReactiveJwtDecoder的setJwtValidator方法签名变更使用spring-boot-starter-webflux但未排除spring-boot-starter-web导致 Servlet 和 WebFlux 混用启动失败。5.7 性能压测网关层 Token 校验的瓶颈不在 CPU而在 GC我们对 Gateway 进行 10k QPS 压测时发现JVM 参数-XX:UseG1GC -XX:MaxGCPauseMillis200下Young GC 频繁每 2 秒一次Prometheus显示jvm_gc_pause_seconds_count{actionend of minor gc}暴涨根本原因是JwtDecoder每次解析都创建大量临时对象Base64解码缓冲区、JSONObject实例解决方案启用 JWT 解析缓存SAS 默认关闭Bean public JwtDecoder jwtDecoder(JWKSourceSecurityContext jwkSource) { NimbusJwtDecoder jwtDecoder (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(https://uaa.example.com); jwtDecoder.setJwtValidator(Validators.createDefault()); // 启用内置缓存 return jwtDecoder; }5.8 安全加固必须禁用的 5 个危险配置配置项危险性正确做法spring.security.oauth2.resourceserver.jwt.jwk-set-uri指向 HTTP 地址中间人攻击JWK 可被篡改必须使用 HTTPS且证书由可信 CA 签发RegisteredClient.redirectUri使用*通配符授权码可被重定向到恶意站点必须精确到具体域名和路径spring.security.oauth2.resourceserver.jwt.public-key-location读取本地文件文件被篡改即验签失效改用jwk-set-uri从 UAA 动态获取spring.cloud.gateway.httpclient.ssl.use-insecure-trust-managertrue忽略 SSL 证书验证生产环境必须设为false并配置信任库spring.security.oauth2.client.registration.*.client-secret明文存储密钥泄露即客户端被冒用使用spring-cloud-config加密或 Vault5.9 故障排查401 Unauthorized 的 7 种根因速查表现象检查点命令/方法Gateway 日志Invalid JWT signatureUAA 的 JWK 公钥是否更新网关是否缓存旧公钥curl https://uaa.example.com/oauth2/jwks对比网关jwk-set-uri返回值UAA 日志Invalid redirect_uri前端发起授权请求的redirect_uri是否与RegisteredClient注册值完全一致检查浏览器 Network Tab 中authorize?redirect_uri参数业务服务收到X-User-IDnullGateway 的AuthenticationFilter是否被Order值覆盖在AuthenticationFilter的filter方法首行加log.info(Auth filter triggered)/oauth2/token返回invalid_grantcode_verifier是否与生成code_challenge时使用的值一致用相同算法重新计算code_challenge并比对Refresh Token 失效Redis 中revocation_set是否存在该 Token 的 hashZSCORE revocation_set token_hashScope 校验失败Token Payload 中scope字段是否为字符串数组还是逗号分隔字符串jwt.io解析 Token确认scope类型Introspection 接口 401Gateway 调用/introspect时的AuthorizationHeader 是否正确curl -H Authorization: Basic base64(client_id:client_secret) https://uaa.example.com/oauth2/introspect5.10 监控告警必须设置的 4 个 Prometheus 指标# alert-rules.yml - alert: UaaJwkUnreachable expr: rate(http_client_requests_seconds_count{uri/oauth2/jwks, status_code~5..}[5m]) 0.1 for: 2m labels: severity: critical annotations: summary: UAA JWK endpoint unreachable - alert: GatewayAuthLatencyHigh expr: histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{handlerAuthenticationFilter}[5m])) by (le)) 0.5 for: 2m labels: severity: warning annotations: summary: Gateway authentication latency 500ms - alert: TokenIntrospectionFailureRateHigh expr: rate(http_client_requests_seconds_count{uri/oauth2/introspect, status_code~5..}[5m]) / rate(http_client_requests_seconds_count{uri/oauth2/introspect}[5m]) 0.05 for: 2m labels: severity: warning annotations: summary: Token introspection failure rate 5% - alert: RevocationListSizeTooLarge expr: redis_sorted_set_length{keyrevocation_set} 100000 for: 10m labels: severity: warning annotations: summary: Revocation list size 100k, consider cleanup job5.11 灰度发布如何零停机升级 UAA 的 JWK 密钥对步骤在 UAA 配置新密钥对但不切换为默认签发密钥将新 JWK 的kid添加到JWKSet保持旧kid仍在集合中更新 Gateway 的jwk-set-uri缓存刷新策略默认 5 分钟或手动触发JwkSetCache.clear()观察日志确认新旧kid的 Token 均能成功解析将 UAA 的默认签发密钥切换为新密钥对等待旧密钥签发的所有 Token 自然过期exp时间再从JWKSet移除旧kid。关键点kid是 JWT Header 中的唯一标识Gateway 会根据kid选择对应公钥验签。只要JWKSet同时包含新旧kid就能平滑过渡。5.12 最后一个忠告不要试图在 Gateway 里实现完整的 OAuth2 Client网上很多教程教你在 Gateway 里写OAuth2AuthorizedClientService、OAuth2AuthorizationRequestResolver这是严重误区。Gateway 的职责是Resource Server验证 Token不是Client发起授权请求。授权流程必须由前端或 Backend-for-FrontendBFF完成Gateway 只接收已签发的 Bearer Token 并校验。否则你会陷入 Cookie 管理、CSRF 防护、Session 同步等与网关定位无关的复杂性泥潭。我在某电商项目就见过团队在 Gateway 里实现完整的 Authorization Code Flow结果因 Netty 的线
返回列表