Skip to content

JWT 集成 ​

CoSec 使用 Auth0 java-jwt 库来创建和验证 JSON Web Token。该集成封装在 cosec-jwt 模块中,提供 JwtTokenConverter(签发令牌)和 JwtTokenVerifier(验证并提取主体)。Spring Boot 自动配置将所有组件组装在一起。

令牌生命周期 ​

令牌有效期默认值 ​

令牌类型默认有效期可通过以下方式配置
访问令牌10 分钟cosec.jwt.token-validity.access
刷新令牌7 天cosec.jwt.token-validity.refresh

这些默认值定义在 JwtProperties 中:

kotlin
data class TokenValidity(
    var access: Duration = Duration.ofMinutes(10),
    var refresh: Duration = Duration.ofDays(7)
)

支持的算法 ​

自动配置支持三种 HMAC 算法,通过 cosec.jwt.algorithm 选择:

值算法Javadoc
HMAC256(默认)HS256Algorithm.HMAC256(secret)
HMAC384HS384Algorithm.HMAC384(secret)
HMAC512HS512Algorithm.HMAC512(secret)

JWT 声明结构 ​

JwtTokenConverter 构建具有以下声明结构的 JWT 访问令牌:

json
{
  "jti": "<generated-unique-id>",
  "sub": "<principal.id>",
  "iat": 1684000000,
  "exp": 1684000600,
  "policies": ["policy-id-1", "policy-id-2"],
  "roles": ["admin", "user"],
  "attributes": {"key": "value"},
  "tenantId": "tenant-123"
}

关键映射:

  • sub(主题):设置为 principal.id -- 唯一用户标识符
  • jti(JWT ID):由 IdGenerator 生成(默认:UUID)。用于令牌吊销(见下文《注销登录(Token 吊销)》小节)和刷新令牌绑定
  • policies: PolicyCapable.POLICY_KEY 声明 -- 分配给主体的策略 ID 列表(仅在非空时写入)
  • roles: RoleCapable.ROLE_KEY 声明 -- 角色 ID 列表(仅在非空时写入)
  • attributes: CoSecPrincipal::attributes.name 声明 -- 任意键值元数据(仅在非空时写入)
  • tenantId: Tenant.TENANT_ID_KEY 声明 -- 仅当主体实现了 TenantCapable 时存在

刷新令牌的结构更简单:

json
{
  "jti": "<refresh-token-id>",
  "sub": "<access-token-id>",
  "iat": 1684000000,
  "exp": 1685209600
}

刷新令牌的 sub 声明被设置为访问令牌的 jti,在两个令牌之间建立绑定关系。

关键类 ​

JwtTokenConverter ​

JwtTokenConverter 实现了 TokenConverter,将 CoSecPrincipal 转换为 CompositeToken:

kotlin
class JwtTokenConverter(
    private val idGenerator: IdGenerator,
    private val algorithm: Algorithm,
    private val accessTokenValidity: Duration = Duration.ofMinutes(10),
    private val refreshTokenValidity: Duration = Duration.ofDays(7)
) : TokenConverter

JwtTokenVerifier ​

JwtTokenVerifier 实现了 TokenVerifier,提供:

  • verify(AccessToken):验证签名,检查过期时间,提取 TokenPrincipal
  • refresh(CompositeToken):验证刷新令牌,验证访问令牌的签名(其有效期可能已过),确保其 sub 与访问令牌的 jti 匹配,然后从(可能已过期的)访问令牌中提取主体

Jwts 工具类 ​

Jwts 提供辅助函数:

  • decode(token):去除 Bearer 前缀并解码 JWT(不验证)
  • toPrincipal(decodedJWT):提取所有声明并构造 TokenPrincipal(当存在 tenantId 时构造 TokenTenantPrincipal)
  • removeBearerPrefix():去除 "Bearer " 前缀的字符串扩展函数(如果存在)

架构图 ​

令牌创建流程 ​

mermaid
sequenceDiagram
    autonumber
    participant Auth as TokenCompositeAuthentication
    participant CA as CompositeAuthentication
    participant AP as AuthenticationProvider
    participant Impl as Authentication Impl
    participant Conv as JwtTokenConverter
    participant JWT as JWT.create()

    Auth->>CA: authenticate(credentials)
    CA->>AP: getRequired(credentialsType)
    AP-->>CA: Authentication instance
    CA->>Impl: authenticate(credentials)
    Impl-->>Auth: CoSecPrincipal
    Auth->>Conv: toToken(principal)
    Conv->>JWT: create access token (sub=policies=roles=tenantId)
    JWT-->>Conv: signed access token string
    Conv->>JWT: create refresh token (sub=accessTokenId)
    JWT-->>Conv: signed refresh token string
    Conv-->>Auth: CompositeToken(accessToken, refreshToken)

令牌验证流程 ​

mermaid
flowchart TD
    A["Incoming AccessToken"] --> B["removeBearerPrefix()"]
    B --> C["jwtVerifier.verify(token)"]
    C --> D{"Verification result"}
    D -->|"TokenExpiredException"| E["throw TokenExpiredException"]
    D -->|"Other Exception"| F["throw TokenVerificationException"]
    D -->|"Valid DecodedJWT"| N{"revoked (jti in TokenStore)?"}
    N -->|"yes"| R["throw TokenRevokedException (401)"]
    N -->|"no"| G["Jwts.toPrincipal(decodedJWT)"]
    G --> H["Extract sub, policies, roles, attributes"]
    H --> I{"tenantId claim present?"}
    I -->|"yes"| J["return TokenTenantPrincipal"]
    I -->|"no"| K["return TokenPrincipal"]

    style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style D fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style E fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style F fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style G fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style H fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style I fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style J fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style K fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style N fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style R fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

刷新令牌流程 ​

mermaid
sequenceDiagram
    autonumber
    participant Client
    participant Verifier as JwtTokenVerifier
    participant JWT as JWT.require()
    participant Jwts as Jwts

    Client->>Verifier: refresh(CompositeToken)
    Verifier->>JWT: verify(refreshToken)
    JWT-->>Verifier: DecodedJWT (refresh)
    Verifier->>Jwts: decode(accessToken) -- signature verified next, expiry not re-checked
    Jwts-->>Verifier: DecodedJWT (access, possibly expired)
    Verifier->>Verifier: verifyAccessTokenSignature(accessJWT)
    Verifier->>Verifier: require(refresh.sub == access.jti)
    Verifier->>Jwts: toPrincipal(accessJWT)
    Jwts-->>Verifier: TokenPrincipal
    Verifier-->>Client: TokenPrincipal

注销登录(Token 吊销) ​

JWT 默认是无状态的——仅在客户端删除令牌并不会让服务端令牌失效。CoSec 通过以令牌的 jti 声明为键的可选吊销机制弥补了这一缺口。

功能说明 ​

  • TokenRevoker.revoke(accessToken) 验证令牌并将其 jti 记录到吊销存储中。请在自定义的登出端点中调用它。
  • 被吊销的访问令牌会立即失效并返回 401——每次验证都会经过 RevocableTokenVerifier,它在接受令牌前会先检查吊销存储。
  • 刷新令牌绑定到访问令牌的 jti(即其 sub 声明),因此被吊销令牌的刷新请求同样会被拒绝。
  • 吊销条目的存活时间为刷新令牌的有效期(cosec.jwt.token-validity.refresh),因此它绝不会早于所绑定的刷新令牌过期。

令牌已失效时登出保持幂等——捕获并忽略验证失败即可:

kotlin
@PostMapping("/logout")
fun logout(@RequestHeader(HttpHeaders.AUTHORIZATION) authorization: String): ResponseEntity<Void> {
    try {
        tokenRevoker.revoke(SimpleAccessToken(authorization)) // Bearer 前缀由验证器内部剥离
    } catch (ignored: TokenVerificationException) {
        // 令牌已无效或已过期——已无可吊销
    }
    return ResponseEntity.noContent().build()
}

注意:请在访问令牌过期前执行注销。已过期的访问令牌会验证失败,此时其仍有效的绑定刷新令牌将无法再通过 TokenRevoker 吊销。

如何启用 ​

yaml
cosec:
  jwt:
    token-revocation:
      enabled: true

这将接入基于 Redis 的 CoCacheTokenStore,它需要 cosec-cocache 依赖(启动器的 cacheSupport Gradle 特性)以及 Redis 连接:

kotlin
dependencies {
    implementation("me.ahoo.cosec:cosec-spring-boot-starter") {
        capabilities {
            requireCapability("me.ahoo.cosec:cosec-spring-boot-starter-cache-support")
        }
    }
}

TokenStore SPI ​

吊销存储通过 TokenStore SPI 实现可插拔:

  • 默认 NoOp —— 无状态空实现。升级 CoSec 后行为零变化,保持无状态默认行为。
  • CoCacheTokenStore —— 开箱即用的 Redis 实现(CoCache 两级缓存:本地 + Redis),在启用该属性且类路径上存在 cosec-cocache 时自动装配。
  • 自定义 —— 提供自己的 TokenStore Bean,可将吊销信息存储到其他任何地方。

运维注意事项 ​

  • 注入模式:通过网关请求头注入安全上下文的下游服务不验证 JWT 签名,因此它们自身无法检查吊销状态。吊销的执行发生在验证边缘(网关)。
  • 传播:CoCache 通过 Redis pub/sub 驱逐本地条目,因此在集群健康时吊销几乎实时地在所有实例上生效。最坏情况下,传播时间受本地缓存 TTL 限制(默认 30 秒,可通过 cosec.authorization.cache.token.* 配置)。
  • Redis 故障时失效(fail-open):在 CoCache 默认的 strictFailure=false 下,Redis 不可达会使 isRevoked 回退为 false(被吊销的令牌可能重新通过认证),吊销写入也会被丢弃。偏好 fail-closed 行为的部署可以设置 cocache.redis.strict-failure=true。

Spring Boot 自动配置 ​

CoSecJwtAutoConfiguration 在以下条件满足时激活:

  1. cosec.enabled=true(默认)
  2. cosec.jwt.enabled=true(默认)
  3. JwtTokenConverter 在类路径上

它注册五个 Bean:

Bean类型用途
cosecTokenAlgorithmAlgorithm来自配置的 HMAC 算法
cosecTokenConverterTokenConverter创建 JWT 令牌
cosecTokenStoreTokenStore吊销存储(默认 NoOp,除非接入缓存实现,见《注销登录(Token 吊销)》小节)
cosecJwtTokenVerifierTokenVerifier验证 JWT 令牌并拒绝已吊销的令牌
cosecTokenRevokerTokenRevoker注销时吊销令牌

当认证也被启用时,它还会注册 TokenCompositeAuthentication,将基于凭据的认证与令牌签发链接在一起。

配置示例 ​

yaml
cosec:
  jwt:
    enabled: true
    algorithm: HMAC256
    secret: your-secret-key-must-be-long-enough
    token-validity:
      access: 10m
      refresh: 7d
    token-revocation:
      enabled: false # 可选启用注销;设为 true 时接入基于 Redis 的 CoCacheTokenStore

吊销的本地缓存通过 cosec.authorization.cache.token.* 调优(默认:写入后 30 秒过期、10 万条目——这也是跨实例登出生效的最坏传播窗口)。

参考文献 ​

相关页面 ​

基于 Apache License 2.0 发布