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 | 令牌抽象层:SecurityTokenDescriptor、SigningCredentials、TokenValidationParameters、TokenValidationResult、密钥类型、签名提供者 |
soulsoft_identity_tokens_jwt | JWT 具体实现:JwtSecurityTokenHandler、JwtHeader、JwtPayload |
额外需要 soulsoft_identity_claims 来构建 Claim 和 ClaimsPrincipal。
快速开始
下面是一个完整的端到端示例:对称密钥签名、生成 JWT、校验 JWT、读取身份。
核心分两步:② 签发阶段 —— SecurityTokenDescriptor 描述令牌内容,createEncodedJwt() 完成序列化 + 签名;⑤ 校验阶段 —— TokenValidationParameters 指定校验密钥和 issuer/audience,返回 Success 或 Failed,不抛异常。
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 时必须传入:
// 对称密钥(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 种算法:
| 类别 | 算法常量 | 密钥类型 |
|---|---|---|
| HMAC | HmacSha256、HmacSha384、HmacSha512 | SymmetricSecurityKey |
| RSA | RsaSha256、RsaSha384、RsaSha512 | RsaSecurityKey |
| RSA-PSS | RsaSsaPssSha256、RsaSsaPssSha384、RsaSsaPssSha512 | RsaSecurityKey |
| ECDSA | EcdsaSha256、EcdsaSha384、EcdsaSha512 | ECDsaSecurityKey |
密钥和算法的匹配由 SupportedAlgorithms 自动检查——SymmetricSecurityKey 只接受 HMAC 系列,AsymmetricSecurityKey 只接受 RSA/ECDSA 系列,不匹配时抛异常。
令牌生命周期:生成与校验
生成——createEncodedJwt(descriptor) 内部分两步:
createToken(descriptor):创建JwtHeader(写入typ="JWT"、alg、kid)和JwtPayload,将descriptor中的 claims、issuer、audience、audiences、notBefore、expires、issuedAt 分别写入对应字段writeToken(token):将 header 和 payload 分别做 Base64Url 编码,从凭据获取SignatureProvider对"header.payload"签名,再将签名 Base64Url 编码,拼接为"header.payload.signature"三段式字符串
关键行为:
descriptor.audience和descriptor.audiences都会写入aud,多值时JwtContent自动合并为数组SignatureProvider有实例缓存(CryptoProviderFactory内ConcurrentHashMap+ 桶级锁),相同密钥+算法不会重复创建
校验——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 的默认值与直觉不完全一致——大部分校验默认开启:
| 参数 | 默认值 | 行为 |
|---|---|---|
validateIssuer | true | 验证 iss 是否在 validIssuers 中 |
validateAudience | true | 验证 aud 是否在 validAudiences 中 |
validateLifetime | true | 验证 exp 和 nbf |
requireExpirationTime | true | 无 exp 则校验失败 |
validateIssuerSigningKey | true | 启用密钥筛选 |
validateTokenReplay | false | 重放检测,需配合 tokenReplayCache |
clockSkew | Duration.Zero | 无时钟偏差容忍 |
注意:clockSkew = Duration.Zero 意味着过期时间和生效时间严格按服务器时钟比对,没有任何缓冲。生产环境中如果签发方和校验方时钟不完全同步,建议设置合理的 clockSkew(如 Duration.minute * 5)。
每个维度都支持自定义委托替换默认逻辑——设置 lifetimeValidator、audienceValidator、issuerValidator、signatureValidator 等后,对应的默认校验被跳过。
结果模型
validateToken() 返回枚举,只有两种可能:
TokenValidationResult.Success(ClaimsIdentity, SecurityToken)— 校验通过,ClaimsIdentity中 claims 来自 JWT payload 的toClaims(),authenticationType来自parameters.authenticationType(默认"AuthenticationTypes.Federation")TokenValidationResult.Failed(Exception)— 校验失败,异常对象描述具体原因
成功返回的 ClaimsIdentity 的 isAuthenticated 为 true(因为 authenticationType 有值)。这意味着校验通过的身份可以直接用于授权判断。
常见用法
非对称密钥签名
生产环境推荐使用非对称密钥。私钥用于签发,公钥部署在校验方:
// 签发方:使用 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 内容而不需要信任它的场景:
let token = "eyJhbGciOi...(JWT 字符串)"
let handler = JwtSecurityTokenHandler()
if (handler.canReadToken(token)) {
let jwt = handler.readJwtToken(token)
println(jwt.payload.iss) // 读取 issuer,但未验签
}canReadToken() 只做格式预检:非空、不超过 250KB、刚好三段。不验证签名。
自定义校验委托
当默认校验逻辑不满足需求时,可以通过委托替换:
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 本身就允许在有效期内重复使用。金融、支付等对重放敏感的场景才需要。
相关专题
- 需要理解身份数据模型 → 身份声明
- 需要将 JWT 校验集成到请求管道 → JWT Bearer 认证
- 需要完整的认证中心 → Identity Server
- 需要了解授权如何消费身份 → 授权总览