Skip to content

Token 与 JWT

本章只对应两个库:

  • soulsoft_identity_tokens
  • soulsoft_identity_tokens_jwt

它们的职责边界很清晰:

  • soulsoft_identity_tokens 提供令牌抽象、密钥、签名凭据、校验参数和校验结果
  • soulsoft_identity_tokens_jwt 把这些抽象落到 JWT,上层入口就是 JwtSecurityTokenHandler

最核心的对象关系是:

text
SecurityTokenDescriptor
    -> JwtSecurityTokenHandler.createToken(...)
    -> JwtSecurityToken
    -> JwtSecurityTokenHandler.writeToken(...)
    -> JWT 字符串

JWT 字符串
    -> JwtSecurityTokenHandler.validateToken(...)
    -> TokenValidationResult
    -> ClaimsIdentity + SecurityToken

最小示例

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

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

// 2. 描述要生成的令牌
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")
]

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

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

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

执行流程

生成流程

  1. SecurityTokenDescriptor 是输入模型,必须在构造时传入 SigningCredentials
  2. createToken(...) 会创建 JwtHeaderJwtPayload
  3. claims 直接写入负载;issueraudienceaudiencesnotBeforeexpiresissuedAt 分别写成 issaudaudnbfexpiat
  4. writeToken(...) 负责序列化和签名。密钥是对称密钥时走 SymmetricSignatureProvider,非对称密钥时走 AsymmetricSignatureProvider

校验流程

  1. validateToken(...) 先验签,再验负载,不会反过来。
  2. 解析阶段默认走 readJwtToken(...);如果设置了 tokenReader,就由它接管读取,但返回值必须是 JwtSecurityToken
  3. 验签成功后,框架继续校验生命周期、受众、发行者、类型和重放。
  4. 全部通过后,框架创建 ClaimsIdentity(parameters.authenticationType),再把 payload.toClaims() 全量加入进去。
  5. 返回值不是抛异常,而是 TokenValidationResult.Success(...)TokenValidationResult.Failed(...)

校验参数与选择规则

维度默认规则自定义入口需要注意
签名issuerSigningKeyResolverissuerSigningKeys 找密钥,任意一个验签成功即可signatureValidatorsignatureValidator 会替换默认验签流程,返回值也必须是 JwtSecurityToken
生命周期默认校验 expnbf,并使用 clockSkewlifetimeValidatorrequireExpirationTime = true 时,没有 exp 直接失败
受众要求令牌里存在 aud,且必须命中 validAudiencesaudienceValidatorrequireAudience = false 且令牌没有 aud 时,可以直接跳过
发行者要求令牌里存在 iss,且必须命中 validIssuersissuerValidator默认是精确匹配,不做模糊比较
类型校验头部 typ 是否命中 validTypestypeValidator只有 typeValidator 但没配 validTypes 时,后续仍会因为类型集合为空而失败
重放只有 validateTokenReplay = true 时才进入重放校验tokenReplayValidator内置逻辑只有在 tokenReplayCache 存在时才会真正拦截重放

JWT 结构在源码里的落点

  • JwtHeader 默认 typ = "JWT",并把 algkid 写入头部
  • JwtPayload 负责 issaudexpnbfiatsubjti 等标准字段访问
  • JwtContent.add(...) 遇到同名键会自动合并成列表,所以多受众最终会落成多个 aud
  • JwtSecurityToken 同时持有 headerpayload 和原始 segments

这意味着:

  • descriptor.audiencedescriptor.audiences 都会进入 aud
  • 多次写入同一个 claim 名称时,最终会保留为一个数组值

结果模型

validateToken(...) 的结果只有两种:

  • TokenValidationResult.Success(ClaimsIdentity, SecurityToken)
  • TokenValidationResult.Failed(Exception)

成功时返回的 ClaimsIdentity 有两个特点:

  • 认证类型来自 TokenValidationParameters.authenticationType
  • claim 集合直接来自 JWT payload 的 toClaims()

默认 authenticationType"AuthenticationTypes.Federation",所以成功结果里的 identity 通常已经是“已认证”状态。

几个容易忽略的细节

canReadToken(...) 只做格式预检

它只检查三件事:

  • token 非空
  • 长度不超过 250 KB
  • 必须刚好有 3 段

它不验证签名,也不验证 issaudexp

readJwtToken(...) 只解析,不验签

它只做:

  • 拆分三段
  • Base64Url 解码
  • 反序列化 header / payload

所以 readJwtToken(...) 适合查看内容,不等于令牌可信。

validateIssuerSigningKey 不等于“跳过签名验证”

这个开关只影响 issuerSigningKeyValidator 是否参与筛选密钥。

真正的签名验证始终还会发生;如果没有任何可用密钥,验签仍然会失败。

当前可用算法就是这些

SecurityAlgorithms 只定义了:

  • HS256 / HS384 / HS512
  • ES256 / ES384 / ES512
  • RS256 / RS384 / RS512
  • PS256 / PS384 / PS512