Skip to content

JWT 令牌

涵盖 JWT 令牌的生成、校验、密钥管理和签名算法——对应 soulsoft_identity_tokens(令牌抽象)和 soulsoft_identity_tokens_jwt(JWT 实现)两个库。

适用场景

  • 需要手动生成和签发 JWT 令牌(如 /token 端点)
  • 需要在校验方配置签名密钥、issuer、audience 等校验参数
  • 需要使用非对称密钥(RSA/ECDSA)替代对称密钥(HMAC)
  • 需要自定义校验逻辑(如自定义 lifetime/audience 验证)

不适合仅使用 JWT Bearer 认证的场景——如果只需要"校验请求中的 Bearer Token",直接使用 JWT Bearer 认证 即可,无需手动调用 validateToken()。两者的关系是:JWT 令牌教你创建和校验 Token,JWT Bearer 教你把这些 Token 用于 HTTP 请求认证——JwtSecurityTokenHandler 是底层 API,JwtBearerHandler 是它在 HTTP 管道中的上层消费者。

前置条件

JWT 令牌处理依赖以下两个包:

用途
soulsoft_identity_tokens令牌抽象层:SecurityTokenDescriptorSigningCredentialsTokenValidationParametersTokenValidationResult、密钥类型、签名提供者
soulsoft_identity_tokens_jwtJWT 具体实现:JwtSecurityTokenHandlerJwtHeaderJwtPayload

额外需要 soulsoft_identity_claims 来构建 ClaimClaimsPrincipal

快速开始

下面是一个完整的端到端示例:对称密钥签名、生成 JWT、校验 JWT、读取身份。

核心分两步:② 签发阶段 —— SecurityTokenDescriptor 描述令牌内容,createEncodedJwt() 完成序列化 + 签名;⑤ 校验阶段 —— TokenValidationParameters 指定校验密钥和 issuer/audience,返回 SuccessFailed不抛异常

cangjie
import std.time.*
import soulsoft_identity_claims.*
import soulsoft_identity_tokens.*
import soulsoft_identity_tokens_jwt.*

// ① 准备对称密钥和签名算法
let key = SymmetricSecurityKey("your-256-bit-secret-your-256-bit-secret")
let credentials = SigningCredentials(key, SecurityAlgorithms.HmacSha256)

// ② 描述要生成的令牌
let descriptor = SecurityTokenDescriptor(credentials)
descriptor.issuer = Some("spire-demo")
descriptor.audience = Some("spire-api")
descriptor.issuedAt = Some(DateTime.nowUTC())
descriptor.notBefore = Some(DateTime.nowUTC())
descriptor.expires = Some(DateTime.nowUTC().addSeconds(3600))
descriptor.claims = [
    Claim(ClaimTypes.Sub, "user-001"),
    Claim(ClaimTypes.Name, "spire"),
    Claim(ClaimTypes.Role, "admin")
]

// ③ 生成 JWT 字符串
let handler = JwtSecurityTokenHandler()
let token = handler.createEncodedJwt(descriptor)

// ④ 配置校验规则
let parameters = TokenValidationParameters()
parameters.validIssuers = ["spire-demo"]
parameters.validAudiences = ["spire-api"]
parameters.issuerSigningKeys = [key]

// ⑤ 校验并读取身份
match (handler.validateToken(token, parameters)) {
    case TokenValidationResult.Success(identity, _) =>
        println(identity.findFirstValue(ClaimTypes.Sub) ?? "")
    case TokenValidationResult.Failed(e) =>
        println(e.message)
}

这段代码跑了完整链路:密钥 → 描述器 → createEncodedJwt() → JWT 字符串 → validateToken() → 先验签、再验负载 → 全部通过返回 ClaimsIdentity。校验默认开启了 issuer、audience、lifetime 三项验证。

核心概念

定位与职责

两个库的职责边界清晰:

soulsoft_identity_tokens               soulsoft_identity_tokens_jwt
    (抽象层)                              (JWT 实现层)
    ├── SecurityTokenDescriptor          ├── JwtSecurityTokenHandler
    ├── SigningCredentials               ├── JwtHeader / JwtPayload
    ├── SecurityKey (对称/非对称)         ├── JwtSerializer
    ├── TokenValidationParameters        └── JwtSegments
    ├── TokenValidationResult
    └── SignatureProvider

    输入:SecurityTokenDescriptor
         ──→ JwtSecurityTokenHandler.createEncodedJwt()
         ──→ JWT 字符串

    JWT 字符串
         ──→ JwtSecurityTokenHandler.validateToken()
         ──→ TokenValidationResult.Success / .Failed

上层入口只有一个:JwtSecurityTokenHandler。生成走 createEncodedJwt(),校验走 validateToken()

密钥与签名凭据

签名凭据由密钥 + 算法组成,构造 SecurityTokenDescriptor 时必须传入:

cangjie
// 对称密钥(HMAC 系列:HS256 / HS384 / HS512)
let symmetricKey = SymmetricSecurityKey("your-256-bit-secret-your-256-bit-secret")
let hmacCredentials = SigningCredentials(symmetricKey, SecurityAlgorithms.HmacSha256)

// RSA 非对称密钥(RS256 / RS384 / RS512 / PS256 / PS384 / PS512)
let rsaKey = RsaSecurityKey.fromPrivateKeyPemFile("private.pem")
let rsaCredentials = SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)

// ECDSA 非对称密钥(ES256 / ES384 / ES512)
let ecKey = ECDsaSecurityKey.fromPrivateKeyPemFile("private.pem")
let ecCredentials = SigningCredentials(ecKey, SecurityAlgorithms.EcdsaSha256)

框架支持三大类共 12 种算法:

类别算法常量密钥类型
HMACHmacSha256HmacSha384HmacSha512SymmetricSecurityKey
RSARsaSha256RsaSha384RsaSha512RsaSecurityKey
RSA-PSSRsaSsaPssSha256RsaSsaPssSha384RsaSsaPssSha512RsaSecurityKey
ECDSAEcdsaSha256EcdsaSha384EcdsaSha512ECDsaSecurityKey

密钥和算法的匹配由 SupportedAlgorithms 自动检查——SymmetricSecurityKey 只接受 HMAC 系列,AsymmetricSecurityKey 只接受 RSA/ECDSA 系列,不匹配时抛异常。

令牌生命周期:生成与校验

生成——createEncodedJwt(descriptor) 内部分两步:

  1. createToken(descriptor):创建 JwtHeader(写入 typ="JWT"algkid)和 JwtPayload,将 descriptor 中的 claims、issuer、audience、audiences、notBefore、expires、issuedAt 分别写入对应字段
  2. writeToken(token):将 header 和 payload 分别做 Base64Url 编码,从凭据获取 SignatureProvider"header.payload" 签名,再将签名 Base64Url 编码,拼接为 "header.payload.signature" 三段式字符串

关键行为:

  • descriptor.audiencedescriptor.audiences 都会写入 aud,多值时 JwtContent 自动合并为数组
  • SignatureProvider 有实例缓存(CryptoProviderFactoryConcurrentHashMap + 桶级锁),相同密钥+算法不会重复创建

校验——validateToken(token, parameters) 严格按顺序执行,不抛异常,统一返回 TokenValidationResult

validateToken(token, params)

  ├── ① 验签:先通过 readJwtToken 解析 JWT,再用密钥验签
  │      ├── issuerSigningKeys / issuerSigningKeyResolver 提供密钥列表
  │      ├── 遍历密钥,任意一个验签成功即通过
  │      └── 全部失败 → TokenValidationResult.Failed

  ├── ② 验负载
  │      ├── validateLifetime:exp < now+clockSkew?nbf > now-clockSkew?
  │      ├── validateAudience:aud 是否在 validAudiences 中?
  │      ├── validateIssuer:iss 是否在 validIssuers 中?
  │      ├── validateTokenType:typ 是否在 validTypes 中?
  │      └── validateTokenReplay:是否重放攻击?(默认关闭)

  └── ③ 全部通过 → 创建 ClaimsIdentity(authenticationType) + 写入 claims
                  → TokenValidationResult.Success

先验签、再验负载,这个顺序是固定的。如果签名不合法,后续的 issuer/audience 校验没有意义。

校验参数

TokenValidationParameters 的默认值与直觉不完全一致——大部分校验默认开启

参数默认值行为
validateIssuertrue验证 iss 是否在 validIssuers
validateAudiencetrue验证 aud 是否在 validAudiences
validateLifetimetrue验证 exp 和 nbf
requireExpirationTimetrue无 exp 则校验失败
validateIssuerSigningKeytrue启用密钥筛选
validateTokenReplayfalse重放检测,需配合 tokenReplayCache
clockSkewDuration.Zero无时钟偏差容忍

注意clockSkew = Duration.Zero 意味着过期时间和生效时间严格按服务器时钟比对,没有任何缓冲。生产环境中如果签发方和校验方时钟不完全同步,建议设置合理的 clockSkew(如 Duration.minute * 5)。

每个维度都支持自定义委托替换默认逻辑——设置 lifetimeValidatoraudienceValidatorissuerValidatorsignatureValidator 等后,对应的默认校验被跳过。

结果模型

validateToken() 返回枚举,只有两种可能:

  • TokenValidationResult.Success(ClaimsIdentity, SecurityToken) — 校验通过,ClaimsIdentity 中 claims 来自 JWT payload 的 toClaims()authenticationType 来自 parameters.authenticationType(默认 "AuthenticationTypes.Federation"
  • TokenValidationResult.Failed(Exception) — 校验失败,异常对象描述具体原因

成功返回的 ClaimsIdentityisAuthenticatedtrue(因为 authenticationType 有值)。这意味着校验通过的身份可以直接用于授权判断。

常见用法

非对称密钥签名

生产环境推荐使用非对称密钥。私钥用于签发,公钥部署在校验方:

cangjie
// 签发方:使用 RSA 私钥
let rsaKey = RsaSecurityKey.fromPrivateKeyPemFile("private.pem")
let credentials = SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256)
let descriptor = SecurityTokenDescriptor(credentials)
descriptor.issuer = Some("spire-demo")
descriptor.expires = Some(DateTime.nowUTC().addSeconds(3600))
let token = JwtSecurityTokenHandler().createEncodedJwt(descriptor)

// 校验方:只部署公钥
let publicKey = RsaSecurityKey.fromPublicKeyPemFile("public.pem")
let parameters = TokenValidationParameters()
parameters.validIssuers = ["spire-demo"]
parameters.issuerSigningKeys = [publicKey]
match (JwtSecurityTokenHandler().validateToken(token, parameters)) {
    case TokenValidationResult.Success(identity, _) => // 校验通过
    case TokenValidationResult.Failed(_) => // 校验失败
}

只解析不验签

readJwtToken() 只做解码和反序列化,不做任何签名校验。适合查看 Token 内容而不需要信任它的场景:

cangjie
let token = "eyJhbGciOi...(JWT 字符串)"
let handler = JwtSecurityTokenHandler()
if (handler.canReadToken(token)) {
    let jwt = handler.readJwtToken(token)
    println(jwt.payload.iss)  // 读取 issuer,但未验签
}

canReadToken() 只做格式预检:非空、不超过 250KB、刚好三段。不验证签名。

自定义校验委托

当默认校验逻辑不满足需求时,可以通过委托替换:

cangjie
let parameters = TokenValidationParameters()
parameters.validIssuers = ["spire-demo"]
parameters.issuerSigningKeys = [key]

// 自定义 lifetime 校验:允许最多 30 秒的时钟偏差
parameters.lifetimeValidator = { (notBefore, expires, token, params) =>
    let now = DateTime.nowUTC()
    if (let Some(exp) <- expires && exp < now.addSeconds(-30)) {
        return false  // 超过 30 秒容忍度
    }
    true
}

设置委托后,对应的默认校验逻辑被跳过——你的委托全权负责该维度的判定。

生产建议

  • 生产环境使用非对称密钥(RSA/ECDSA)。对称密钥要求签发方和校验方持有同一密钥,密钥泄露面大;非对称密钥只需在校验方部署公钥,私钥仅签发方持有。
  • 始终开启 issuer、audience、lifetime 校验。三个参数默认都是 true,不要为了开发方便而关闭——一旦 Token 被用于非预期的服务或超期使用,关闭校验会让你完全失去防护。
  • canReadToken() 通过 ≠ Token 可信。它只是格式预检(非空、≤250KB、三段式),不含任何签名或声明验证。只有 validateToken() 返回 Success 才是可信的。
  • 合理设置 clockSkew。框架默认 Duration.Zero 是无容忍,签发方和校验方时钟哪怕差 1 秒也会导致校验失败。分布式部署时建议设 2-5 分钟的时钟偏差。
  • 校验方不持有私钥。即使使用非对称密钥,也只在校验方部署公钥,私钥仅保留在签发方。

常见问题

validateToken() 和 JWT Bearer 认证有什么关系

JWT Bearer 认证内部调用了 JwtSecurityTokenHandler.validateToken() 来校验请求中的 Token。两层的分工是:validateToken() 负责"这个 Token 本身是否合法",JWT Bearer 负责"如何从 HTTP 请求中提取 Token、如何把校验结果写入 HttpContext.user"。

readJwtToken()validateToken() 怎么选

需要信任 Token 内容时用 validateToken(),只是查看内容(如日志、调试)用 readJwtToken()。前者验签 + 验声明,后者只解码。

什么时候用 validateTokenReplay

只有在你维护了 tokenReplayCache 且业务上需要防止同一个 Token 被多次使用时才开启。API 场景通常不开启——Bearer Token 本身就允许在有效期内重复使用。金融、支付等对重放敏感的场景才需要。

相关专题