- 网络安全
- 认证鉴权
- 后端
【免费下载链接】jose
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
JWTClaimVerificationOptions 是 jose 库中用于配置 JWT Claims Set(即 JWT 载荷部分)校验规则的接口,覆盖身份类声明(iss、aud、sub、typ)的期望值匹配、时间类声明(iat、nbf、exp)的容差与时效校验,以及必填声明集合的强制存在性检查。本文以 JWTClaimVerificationOptions.md 为骨架,结合 validateClaimsSet 实现 与 verify 测试套件,逐一讲解每个选项的含义、类型、默认行为及底层判定逻辑,读完即可在 jwtVerify、jwtDecrypt、UnsecuredJWT 等场景中精确配置 Claims Set 校验。
一、接口定位:它出现在哪些 API 中
该接口定义于 src/types.d.ts,被三个入口共用:
- src/jwt/verify.ts 中的
JWTVerifyOptions(extends VerifyOptions, JWTClaimVerificationOptions),供 jwtVerify() 在完成 JWS 签名验证后校验载荷; - src/jwt/decrypt.ts 中的
JWTDecryptOptions,供 jwtDecrypt() 在完成 JWE 解密后校验明文载荷; - src/jwt/unsecured.ts 中的
UnsecuredJWT.decode()选项。
从调用链看,三者最终都汇聚到同一个实现函数 validateClaimsSet(protectedHeader, encodedPayload, options)。该函数先解析 JSON 载荷,再依次执行 typ 头校验、声明存在性检查、iss/sub/aud 值匹配、nbf/exp/iat 时间戳判定,最后返回JWTPayload。因此,理解这一个接口就等于理解了 jose 全库的 JWT 载荷校验策略。
二、属性总览
该接口全部 8 个属性均为可选(?),未配置时对应校验直接跳过:
| 属性 | 类型 | 作用对象 | 核心作用 |
|---|---|---|---|
audience? | string \| string[] | aud声明 | 期望的 Audience 值,设置后强制aud必须存在 |
issuer? | string \| string[] | iss声明 | 期望的 Issuer 值,设置后强制iss必须存在 |
subject? | string | sub声明 | 期望的 Subject 值,设置后强制sub必须存在 |
typ? | string | typ头参数 | 期望的 JWT Type 头值,设置后强制typ头必须存在 |
maxTokenAge? | string \| number | iat声明 | 允许的最大 Token 年龄,设置后强制iat必须存在 |
requiredClaims? | string[] | 任意声明 | 额外强制必须出现的声明名列表 |
clockTolerance? | string \| number | nbf、exp、iat | 时钟偏差容忍秒数 |
currentDate? | Date | 所有 NumericDate 声明 | 参与时间比较的基准时刻,默认new Date() |
三、身份类声明校验:issuer、audience、subject、typ
这四类选项的共性在于:一旦配置,对应声明/头的"存在"与"值匹配"会同时被强制要求(存在性检查详见下一节 requiredClaims 的默认行为)。
3.1 issuer(Issuer,签发者)
期望的iss值,可以是单个字符串或字符串数组(任一匹配即通过)。源码中的判定为(src/lib/jwt_claims_set.ts#L174-L179):
if ( issuer !== undefined && !((Array.isArray(issuer) ? issuer : [issuer]) as unknown[]).includes(payload.iss!) ) { unexpectedClaim(payload, 'iss') }即:单个字符串会被包装成数组后用includes做全等匹配;不匹配时抛出JWTClaimValidationFailed,claim为'iss',reason为'check_failed'。
3.2 audience(Audience,受众)
期望的aud值,同样支持单个字符串或数组。由于aud本身既可以是字符串也可以是字符串数组,源码采用了专门的处理函数(src/lib/jwt_claims_set.ts#L83-L95):
const checkAudiencePresence = (audPayload: unknown, audOption: unknown[]) => { if (typeof audPayload === 'string') { return audOption.includes(audPayload) } if (Array.isArray(audPayload)) { // Each principal intended to process the JWT MUST // identify itself with a value in the audience claim return audOption.some((aud) => audPayload.includes(aud)) } return false }要点:
- 载荷中
aud是字符串时,必须与期望值全等; - 载荷中
aud是数组时,遵循 RFC 7519 语义:每个处理 JWT 的主体必须在aud中找到自己的标识,因此要求期望数组与载荷数组存在交集(some+includes); - 载荷中
aud缺失或类型非法(既非字符串也非数组)时,返回false,校验失败。
3.3 subject(Subject,主体)
期望的sub值,仅支持单个字符串(src/lib/jwt_claims_set.ts#L181-L183):payload.sub !== subject即失败。注意sub的期望值比较是严格全等,且不像aud那样有数组形式。
3.4 typ(Type,类型头参数)
与前三者不同,typ校验的对象是JWT 的 protected header 而非载荷声明(src/lib/jwt_claims_set.ts#L140-L152)。它要求 protected header 中必须存在字符串类型的typ,且规范化后与期望值相等。规范化规则见 normalizeTyp:
const normalizeTyp = (value: string) => { const normalized = value.toLowerCase() return value.includes('/') ? normalized : `application/${normalized}` }即:期望值不包含/时会被自动补上application/前缀,匹配也忽略大小写。例如传入typ: 'JWT',实际期望头值为application/JWT;传入typ: 'application/jwt'也能匹配application/JWT。校验失败时抛出JWTClaimValidationFailed,claim为'typ'。
四、时间类声明校验:clockTolerance、currentDate、maxTokenAge
这三者共同控制 jose 对 NumericDate 声明(iat、nbf、exp)的时间比较行为。比较基准统一取自currentDate(默认new Date()),并通过 epoch()(Math.floor(date.getTime() / 1000))换算为 Unix 秒。
4.1 currentDate:可控的时间基准
类型为Date,默认值为new Date()(调用时刻的当前时间)。源码在 src/lib/jwt_claims_set.ts#L205-L209 中将其转换为秒并经过validateInput有限性校验。它允许在测试环境或离线场景中"伪造"当前时刻,从而稳定地验证过期、未生效等时间相关逻辑;传入非法值(如NaN)会抛出TypeError('Invalid currentDate option input')。
4.2 clockTolerance:时钟偏差容忍度
用于吸收签发方与验证方之间时钟不同步造成的误判:
- 传入
number(如5)时直接视为秒数; - 传入
string(如"5 seconds"、"10 minutes"、"2 hours")时,由 secs() 解析为秒数。
其作用于三处比较(src/lib/jwt_claims_set.ts#L214-L231):
nbf(生效时间):当nbf > now + tolerance时判定失败;exp(过期时间):当exp <= now - tolerance时抛出JWTExpired;iat(签发时间,仅在设置maxTokenAge时):见 4.3。
也就是说,clockTolerance为验证方"往前和往后"各放宽了容忍窗口。测试用例(test/jwt/verify.test.ts#L324-L330)同时验证了数字形式clockTolerance: 1与字符串形式clockTolerance: '1s'的等价性;L541-L544 则确认传入NaN、Infinity、-Infinity时会被拒绝(TypeError)。
4.3 maxTokenAge:Token 最大年龄
该选项用于限制 Token 从签发(iat)到被验证时刻之间的最大时间跨度:
- 数字形式按秒计(如
5),字符串形式同样经secs()解析(如"10 minutes"); - 设置该选项后,
iat声明变为必需(见下节默认存在性规则); - 校验逻辑见 src/lib/jwt_claims_set.ts#L232-L256:
const age = now - iat! const max = validateInput( 'maxTokenAge option', typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge), ) if (age - tolerance > max) { throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', ...) } if (age < -tolerance) { throw new JWTClaimValidationFailed( '"iat" claim timestamp check failed (it should be in the past)', ...) }两个方向的判定:
age - tolerance > max:Token 太老,抛出JWTExpired(码ERR_JWT_EXPIRED);age < -tolerance:iat落在未来且超出容忍范围,说明签发时间非法,抛出JWTClaimValidationFailed。
注意clockTolerance在这里同样生效:它同时放宽"年龄上限"和"未来签发"两种判定。典型场景是"会话有效期控制":签发方在登录时写入iat,验证方通过maxTokenAge: '7 days'强制会话最多存活 7 天,即使exp未设置或设置更长也无效。
五、存在性校验:requiredClaims 及其默认规则
requiredClaims接收一个字符串数组,数组中的每个声明名必须出现在 JWT Claims Set 中(仅检查存在,不检查值),缺失时抛出JWTClaimValidationFailed,reason为'missing'。
其默认行为是自动叠加由其他选项推导出的必填声明(src/lib/jwt_claims_set.ts#L154-L172):
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options const presenceCheck = [...requiredClaims] if (maxTokenAge !== undefined) presenceCheck.push('iat') if (audience !== undefined) presenceCheck.push('aud') if (subject !== undefined) presenceCheck.push('sub') if (issuer !== undefined) presenceCheck.push('iss') for (const claim of new Set(presenceCheck.reverse())) { if (!Object.hasOwn(payload, claim)) { throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, ...) } }规则归纳:
| 配置的选项 | 被强制要求存在的声明 |
|---|---|
issuer被设置 | iss |
audience被设置 | aud |
subject被设置 | sub |
maxTokenAge被设置 | iat |
requiredClaims中列出 | 所列全部声明 |
实现细节:presenceCheck.reverse()后放入Set去重,保证无论用户自定义列表顺序如何,都不重复检查、也不遗漏。Object.hasOwn仅检查自有属性,因此__proto__之类继承属性不会误判为存在。
六、时间字符串解析:secs() 与支持的格式
clockTolerance与maxTokenAge的字符串形式统一由 secs(str) 解析。其正则(src/lib/jwt_claims_set.ts#L17-L18)支持:
- 单位:
s/secs/second(s)、m/mins/minute(s)、h/hrs/hour(s)、d/day(s)、w/week(s)、y/yrs/year(s); - 数值:整数或小数(如
1.5 hours),允许前置空格; - 方向后缀:
ago(过去,取负值)与from now(未来,取正值),但不能与正负号同时使用; - 匹配忽略大小写。
各单位换算系数见 multipliers:s=1、m=60、h=3600、d=86400、w=604800、y=31557600(一年按 365.25 天计)。格式非法时抛出TypeError('Invalid time period format')。常见写法示例:
{ clockTolerance: 5, // 秒(数字) clockTolerance: '5 seconds', // 秒(字符串) clockTolerance: '10 minutes', // 600 秒 clockTolerance: '2 hours', // 7200 秒 maxTokenAge: '7 days', // 604800 秒 }七、配套错误类型:校验失败如何处理
Claims Set 校验失败时抛出两类错误,二者都实现 JWTClaimValidationFailure 结构(含claim、reason、payload三个字段),类定义在 src/util/errors.ts:
- JWTClaimValidationFailed:
code为ERR_JWT_CLAIM_VALIDATION_FAILED,覆盖值不匹配(check_failed)、类型非法(invalid)、声明缺失(missing)等所有非过期类失败; - JWTExpired:
code为ERR_JWT_EXPIRED,专门用于exp过期与maxTokenAge超龄两类"过期"判定。
注意:JWTExpired并不继承JWTClaimValidationFailed(src/util/errors.ts 的注释明确说明这一点),因此单独用instanceof JWTClaimValidationFailed无法覆盖过期场景。官方推荐的写法是使用联合类型 JWTClaimValidationError 或按err.code判别:
import { jwtVerify } from 'jose' import { errors } from 'jose' try { await jwtVerify(jwt, key, { issuer: 'urn:example:issuer', audience: 'urn:example:audience' }) } catch (err) { switch (err.code) { case 'ERR_JWT_EXPIRED': // 处理过期(err 收窄为 JWTExpired) break case 'ERR_JWT_CLAIM_VALIDATION_FAILED': // 处理其他声明校验失败(err 收窄为 JWTClaimValidationFailed) break default: throw err } }另一个关键事实:Claims Set 校验总是发生在签名验证(或解密)成功之后(见 src/jwt/verify.ts#L174-L187 中verifyCompact之后才调用validateClaimsSet),因此捕获到上述错误时,可以确信载荷确实来自持有私钥/密钥的签发方。
八、组合实战:一个完整的验证配置
结合 jwtVerify() 文档中的对称密钥示例,一个同时使用身份与时间校验的完整配置如下:
import { jwtVerify, createRemoteJWKSet } from 'jose' // 方式一:对称密钥 const secret = new TextEncoder().encode( 'cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2', ) const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6dHJ1ZSwiaWF0IjoxNjY5MDU2MjMxLCJpc3MiOiJ1cm46ZXhhbXBsZTppc3N1ZXIiLCJhdWQiOiJ1cm46ZXhhbXBsZTphdWRpZW5jZSJ9.C4iSlLfAUMBq--wnC6VqD9gEOhwpRZpoRarE0m7KEnI' const { payload, protectedHeader } = await jwtVerify(jwt, secret, { issuer: 'urn:example:issuer', // 强制 iss 存在且匹配 audience: 'urn:example:audience', // 强制 aud 存在且匹配 subject: 'urn:example:subject', // 强制 sub 存在且匹配 typ: 'JWT', // 强制 protected header 的 typ 为 application/JWT maxTokenAge: '5 minutes', // 强制 iat 存在,且 Token 年龄不超过 5 分钟 clockTolerance: '30 seconds', // 时间比较放宽 30 秒,吸收时钟偏差 requiredClaims: ['urn:example:claim'], // 额外强制自定义声明存在 }) console.log(protectedHeader) console.log(payload)// 方式二:远程 JWKS + 动态密钥解析 const JWKS = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs')) const { payload, protectedHeader, key } = await jwtVerify(jwt, JWKS, { issuer: 'urn:example:issuer', audience: 'urn:example:audience', })方式二演示了jwtVerify的 getKey 重载:传入密钥解析函数时,返回值中会额外携带key字段(即实际用于验证的密钥,见 src/jwt/verify.ts#L184-L186)。远程 JWKS 的创建与缓存行为见 createRemoteJWKSet。
生产环境建议的最小配置组合是:issuer+audience+maxTokenAge(或依赖exp)+ 适度的clockTolerance。这样既验证了 Token 的身份归属(防伪造与串用),又通过时间窗口防止重放与长期有效 Token 的滥用。
九、测试佐证与边界行为
test/jwt/verify.test.ts 是验证该接口行为最直接的证据,除上文提及的用例外,还包括:
clockTolerance: 0时不产生任何放宽(L565),即默认零容忍;currentDate可人为拨动系统时间用于测试过期/未生效逻辑;- 非法数值(
NaN、±Infinity)传入clockTolerance、maxTokenAge会抛TypeError(src/lib/jwt_claims_set.ts#L203、L234-L237); aud缺失或格式错误时校验失败,且validateAudienceClaim会在签发侧(JWTClaimsBuilder.setAudience)就拒绝非字符串/非字符串数组的输入,保证签发出的 JWT 载荷形态合法。
此外,签发侧的 JWTClaimsBuilder(被 SignJWT 与 EncryptJWT 使用)与验证侧共享同一套声明形态校验:setIssuer/setSubject/setJti要求字符串,setAudience要求字符串或字符串数组,setNotBefore/setExpirationTime/setIssuedAt支持数字(Unix 秒)、Date或时间字符串(经secs()解析,setIssuedAt无参时默认取当前时刻)。理解两端的一致性,有助于在"签发方"与"验证方"之间设计出对称、可预期的校验配置。
十、小结:选型速查
| 需求 | 选项 | 连带效应 |
|---|---|---|
| 校验签发者 | issuer | 强制iss存在 |
| 校验受众 | audience | 强制aud存在 |
| 校验主体 | subject | 强制sub存在 |
| 校验头类型 | typ | 强制 protected header 的typ存在 |
| 限制 Token 年龄 | maxTokenAge | 强制iat存在 |
| 吸收时钟偏差 | clockTolerance | 放宽nbf/exp/iat判定窗口 |
| 固定比较基准时刻 | currentDate | 替代new Date()参与比较 |
| 追加必填声明 | requiredClaims | 叠加在上述自动规则之上 |
JWTClaimVerificationOptions 的全部选项都遵循"可选、不配置即跳过"的设计哲学,让开发者按需渐进加固校验策略;而所有校验最终都统一收敛于 validateClaimsSet,保证了 jwtVerify、jwtDecrypt、UnsecuredJWT 三条路径上行为完全一致。
- 网络安全
- 认证鉴权
- 后端
【免费下载链接】jose
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
相关推荐
jose 中的 decodeJwt:免验签解析 JWT Claims Set 的完整指南
jose 中的 decodeJwt:免验签解析 JWT Claims Set 的完整指南 decodeJwt 是 jose 库中用于解析 JSON Web To
网络安全认证鉴权后端jose 中 jwtVerify 函数完全指南:JWT 签名验证与 Claims Set 校验实战
jose 中 jwtVerify 函数完全指南:JWT 签名验证与 Claims Set 校验实战 jose 项目为 Node.js、浏览器、Cloudflar
网络安全认证鉴权后端jose JWT 验证实战:jwtVerify 的签名验证与 Claims Set 校验全指南
jose JWT 验证实战:jwtVerify 的签名验证与 Claims Set 校验全指南 导读 本指南聚焦 jose 库中 JWT(JSON Web To
网络安全认证鉴权后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考