bigywater.tech
Spring · 110

JWT 资源服务器:签名验证、时钟偏差与密钥轮换

JWT 资源服务器默认校验了什么、没校验什么?roles 声明为什么 hasRole 不认?时钟偏差有多大容忍?密钥轮换的时候旧令牌会怎么样?这篇在 Spring Boot 4.0.8、Spring Security 7 上自己签令牌、自己起 JWKS 服务逐项实测。

iss 不对也通过
默认的 JwtValidators.createDefault() 只校验时间,别的签发者的令牌返回 200
60 秒
默认时钟偏差容忍:过期 30 秒通过,过期 90 秒 401
旧 key 立即失效
JWKS 换成新 key 之后,k1 令牌马上 401,没有宽限期

默认校验

实测 · 本机执行输出
  不带令牌 GET /read → HTTP 401,WWW-Authenticate: Bearer resource_metadata="http…
  有效令牌 GET /read → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:34.529572Z], SCOPE_read, SCOPE_write]
  有效令牌 GET /admin(令牌里有 roles = [ADMIN]) → HTTP 403,WWW-Authenticate: Bearer error="insufficient_sco… error=insufficient_scope description=The request requires higher privileges than provided by the access token.
  没有 scope 的令牌 GET /read → HTTP 403,WWW-Authenticate: Bearer error="insufficient_sco… error=insufficient_scope description=The request requires higher privileges than provided by the access token.
  篡改了 payload(sub 改成 mallory)的令牌 GET /read → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature
  alg = none 的令牌 GET /read → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=Unsupported algorithm of none
  alg = HS256、用公钥字节当密钥的令牌 GET /read → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
  过期 90 秒的令牌 GET /read → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Jwt expired at 2026-10-20T04:22:02Z
  nbf 在 120 秒之后的令牌 GET /read → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Jwt used before 2026-10-20T04:25:32Z
  iss 是别的签发者的令牌 GET /read → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:34.561582Z], SCOPE_read, SCOPE_write]

签名与算法被严格检查;时间有 60 秒容忍;签发者默认不校验;scope 映射成 SCOPE_,roles 被忽略。

时钟与签发者

实测 · 本机执行输出
  过期 30 秒的令牌(默认) → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:35.954695Z], SCOPE_read, SCOPE_write]
  过期 90 秒的令牌(默认) → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Jwt expired at 2026-10-20T04:22:02Z
  过期 30 秒的令牌,JwtTimestampValidator(Duration.ZERO) → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Jwt expired at 2026-10-20T04:23:02Z
  iss 不对的令牌,JwtValidators.createDefaultWithIssuer → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: The iss claim is not valid
  iss 正确的令牌,createDefaultWithIssuer → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:38.927857Z], SCOPE_read, SCOPE_write]

JwtTimestampValidator 可调容忍;createDefaultWithIssuer 校验 iss。

JWKS 轮换

实测 · 本机执行输出
  第 1 步 JWKS = {k1}:k1 令牌 → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:40.356155Z], SCOPE_read, SCOPE_write]
    此时 JWKS 服务被访问了 1 次
  第 2 步 JWKS 切成 {k2} 之后立刻:k2 令牌(kid 不在缓存里) → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:40.374228Z], SCOPE_read, SCOPE_write]
    这一步 JWKS 服务又被访问了 1 次
  第 3 步:k1 令牌(k2 令牌触发的那次刷新已经把缓存换成了 {k2}) → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
  第 4 步 等 16 秒之后:k2 令牌 → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:56.390336Z], SCOPE_read, SCOPE_write]
  第 5 步:k1 令牌(JWKS 里已经没有 k1) → HTTP 401,WWW-Authenticate: Bearer error="invalid_token", … error=invalid_token description=An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
    累计 JWKS 服务被访问 4 次
  第 6 步 JWKS 服务宕机:k2 令牌(已缓存) → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:56.398411Z], SCOPE_read, SCOPE_write]
  JWKS 服务宕机时刚启动的应用:k2 令牌 → HTTP 401,WWW-Authenticate: Bearer resource_metadata="http…

kid 不在缓存时立即重新获取;旧 key 立刻失效;JWKS 宕机时已缓存的 key 可用。

authorities

实测 · 本机执行输出
  GET /read(令牌 scope = "read write") → HTTP 200,/read 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:59.248369Z], SCOPE_read, SCOPE_write]
  GET /anything → HTTP 200,/anything 用户=alice authorities=[FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-10-20T04:23:59.266118Z], SCOPE_read, SCOPE_write]

scope 映射成 SCOPE_xxx;roles 要自己写转换器。

分步演示

JWT 资源服务器
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Spring Security 7.0.7、Nimbus JOSE JWT 10.4)。RS256 令牌由我自己用 Nimbus 生成(两把密钥 k1、k2);JWKS 由一个本地 Python http.server 提供,可切换文件和模拟宕机;JwtDecoder 是 NimbusJwtDecoder.withJwkSetUri 构造的,校验器通过 -Dx110.mode 切换;JWKS 的缓存时长我没有读配置,只通过上述步骤观察行为
    • 没有验证:JWKS 缓存的 TTL 具体是多少、重新获取的限流(这里只看到了 16 秒后的结果);issuer-uri 自动发现;不透明令牌(introspection);aud 受众校验;多个签发者(JwtIssuerAuthenticationManagerResolver);令牌撤销与黑名单;DPoP 与 mTLS 绑定令牌;密钥轮换的服务端(授权服务器)如何发布新旧两把 key
    京ICP备2026067502号-1京公网安备11010502063487号