Token 与 JWT
本章只对应两个库:
soulsoft_identity_tokenssoulsoft_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)
}执行流程
生成流程
SecurityTokenDescriptor是输入模型,必须在构造时传入SigningCredentials。createToken(...)会创建JwtHeader和JwtPayload。claims直接写入负载;issuer、audience、audiences、notBefore、expires、issuedAt分别写成iss、aud、aud、nbf、exp、iat。writeToken(...)负责序列化和签名。密钥是对称密钥时走SymmetricSignatureProvider,非对称密钥时走AsymmetricSignatureProvider。
校验流程
validateToken(...)先验签,再验负载,不会反过来。- 解析阶段默认走
readJwtToken(...);如果设置了tokenReader,就由它接管读取,但返回值必须是JwtSecurityToken。 - 验签成功后,框架继续校验生命周期、受众、发行者、类型和重放。
- 全部通过后,框架创建
ClaimsIdentity(parameters.authenticationType),再把payload.toClaims()全量加入进去。 - 返回值不是抛异常,而是
TokenValidationResult.Success(...)或TokenValidationResult.Failed(...)。
校验参数与选择规则
| 维度 | 默认规则 | 自定义入口 | 需要注意 |
|---|---|---|---|
| 签名 | 用 issuerSigningKeyResolver 或 issuerSigningKeys 找密钥,任意一个验签成功即可 | signatureValidator | signatureValidator 会替换默认验签流程,返回值也必须是 JwtSecurityToken |
| 生命周期 | 默认校验 exp、nbf,并使用 clockSkew | lifetimeValidator | requireExpirationTime = true 时,没有 exp 直接失败 |
| 受众 | 要求令牌里存在 aud,且必须命中 validAudiences | audienceValidator | requireAudience = false 且令牌没有 aud 时,可以直接跳过 |
| 发行者 | 要求令牌里存在 iss,且必须命中 validIssuers | issuerValidator | 默认是精确匹配,不做模糊比较 |
| 类型 | 校验头部 typ 是否命中 validTypes | typeValidator | 只有 typeValidator 但没配 validTypes 时,后续仍会因为类型集合为空而失败 |
| 重放 | 只有 validateTokenReplay = true 时才进入重放校验 | tokenReplayValidator | 内置逻辑只有在 tokenReplayCache 存在时才会真正拦截重放 |
JWT 结构在源码里的落点
JwtHeader默认typ = "JWT",并把alg、kid写入头部JwtPayload负责iss、aud、exp、nbf、iat、sub、jti等标准字段访问JwtContent.add(...)遇到同名键会自动合并成列表,所以多受众最终会落成多个audJwtSecurityToken同时持有header、payload和原始segments
这意味着:
descriptor.audience和descriptor.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 段
它不验证签名,也不验证 iss、aud、exp。
readJwtToken(...) 只解析,不验签
它只做:
- 拆分三段
- Base64Url 解码
- 反序列化 header / payload
所以 readJwtToken(...) 适合查看内容,不等于令牌可信。
validateIssuerSigningKey 不等于“跳过签名验证”
这个开关只影响 issuerSigningKeyValidator 是否参与筛选密钥。
真正的签名验证始终还会发生;如果没有任何可用密钥,验签仍然会失败。
当前可用算法就是这些
SecurityAlgorithms 只定义了:
HS256/HS384/HS512ES256/ES384/ES512RS256/RS384/RS512PS256/PS384/PS512