- 网络安全
- 认证鉴权
- 后端
【免费下载链接】jose
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
本篇技术指南围绕 jose 仓库中 ProduceJWT 接口 展开,深入讲解该接口定义的七个 JWT 标准 Claim 设置方法(iss、sub、aud、jti、nbf、exp、iat)、链式调用设计以及时间输入的三态解析规则。读者读完将掌握 SignJWT、EncryptJWT、UnsecuredJWT 三类 JWT 生产类共用的 Claims 构建能力,并能结合源码理解时间跨度字符串的底层解析实现与参数校验逻辑。
ProduceJWT 是什么
ProduceJWT是 jose 中定义在 src/types.d.ts 的 TypeScript 接口,其文档自述为 "Generic interface for JWT producing classes"(JWT 生产类的通用接口)。它把"向 JWT Claims Set 写入标准声明"这一行为抽象成统一契约,凡是实现了该接口的类,都可以用同一套链式 API 来填充 JWT 载荷。
在 jose 中,以下三个类都继承自JWTClaimsBuilder(src/lib/jwt_claims_set.ts),因此全部满足ProduceJWT契约:
SignJWT(src/jwt/sign.ts):用于构建并签名 Compact JWS 格式的 JWT;EncryptJWT(src/jwt/encrypt.ts):用于构建并加密 Compact JWE 格式的 JWT;UnsecuredJWT(src/jwt/unsecured.ts):用于处理{ "alg": "none" }的未签名、未加密 JWT。
从源码可以确认,SignJWT_base、EncryptJWT_base、UnsecuredJWT_base三者的类型都被声明为new (payload?: types.JWTPayload) => types.ProduceJWT,这正是接口作为"生产端统一类型"的体现。
七个方法:完整的标准 Claim 写入能力
ProduceJWT共声明 7 个方法,全部返回this,从而支持无中断的链式调用。它们覆盖了 RFC 7519 中 JWT Claims Set 的全部标准注册声明:
| 方法 | 对应 Claim | 参数类型 | 说明 |
|---|---|---|---|
setIssuer(issuer) | iss(Issuer,签发者) | string | 必须为字符串 |
setSubject(subject) | sub(Subject,主体) | string | 必须为字符串 |
setAudience(audience) | aud(Audience,受众) | string \| string[] | 字符串或字符串数组 |
setJti(jwtId) | jti(JWT ID) | string | 用于防止重放的唯一标识 |
setNotBefore(input) | nbf(Not Before,生效时间) | number \| string \| Date | 三态时间输入,见下文 |
setExpirationTime(input) | exp(Expiration Time,过期时间) | number \| string \| Date | 三态时间输入,见下文 |
setIssuedAt(input?) | iat(Issued At,签发时间) | number \| string \| Date(可选) | 不传参数则使用当前时间戳 |
字符串类 Claim 的强校验
对于iss、sub、jti三个字符串型 Claim,实现层在 src/lib/jwt_claims_set.ts 做了严格类型检查:非字符串输入会抛出TypeError(如"iss" claim must be a string);aud则必须是字符串或"全部由字符串组成的数组",否则抛出"aud" claim must be a string or an array of strings。测试用例 test/jwt/sign.test.ts 验证了这些边界行为,例如setIssuer(0)、setSubject(null)、setJti({})、setAudience(['audience', 0])都会被拒绝。
时间类 Claim 的三态输入
setExpirationTime、setNotBefore与setIssuedAt接受三种输入,语义完全一致:
number:直接作为 Unix 时间戳使用(以秒为单位);Date:内部通过Math.floor(date.getTime() / 1000)(源码中的epoch函数,见 src/lib/jwt_claims_set.ts)转换为 Unix 时间戳;string:解析为"相对于当前 Unix 时间戳的时间跨度",即当前时间 + 时间跨度。
其中时间字符串的解析由secs()函数(src/lib/jwt_claims_set.ts)完成。它使用正则
/^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i并按单位换算系数(src/lib/jwt_claims_set.ts)计算秒数:
| 单位 | 系数(秒) | 合法的拼写变体 |
|---|---|---|
| 秒 | 1 | sec、secs、second、seconds、s |
| 分钟 | 60 | minute、minutes、min、mins、m |
| 小时 | 3600 | hour、hours、hr、hrs、h |
| 天 | 86400 | day、days、d |
| 周 | 604800 | week、weeks、w |
| 年 | 31557600(365.25 天) | year、years、yr、yrs、y |
注意两个细节:月份不被支持(没有month/mo单位);一年被定义为 365.25 天,即 31557600 秒,这与 RFC 7519 对 NumericDate 的定义保持一致。
时间跨度还支持方向语义:
- 前置
-(如'-1 hour')或后置ago(如'1 hour ago')表示从当前时间减去该跨度; - 后置
from now(如'1 hour from now')仅用于增强可读性,语义等同于直接加法,最终都作用于当前 Unix 时间戳; - 同时使用
-与ago会被拒绝(正则中matched[4] && matched[1]判定非法),测试 test/unit/secs.test.ts 覆盖了'1 fortnight'、'= 1 second'、'- 1 second ago'等非法格式,均抛出TypeError: Invalid time period format。
setIssuedAt略有特殊:参数可选,不传参数时直接取epoch(new Date()),即以调用时刻的当前时间戳作为iat。其余两个方法必须传参。
链式调用与最终序列化
ProduceJWT的所有方法返回this,这是 jose 生产端 API 的核心体验。一个典型的完整用法如下(源自 SignJWT 文档示例):
const secret = new TextEncoder().encode( 'cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2', ) const alg = 'HS256' const jwt = await new jose.SignJWT({ 'urn:example:claim': true }) .setProtectedHeader({ alg }) .setIssuedAt() .setIssuer('urn:example:issuer') .setAudience('urn:example:audience') .setExpirationTime('2h') .sign(secret) console.log(jwt)对应的加密版本(EncryptJWT 文档示例)结构完全一致,仅把setProtectedHeader({ alg })换成同时包含alg与enc的头部,并把终端方法sign()换成encrypt():
const secret = jose.base64url.decode('zH4NRP1HMALxxCFnRZABFA7GOJtzU_gIj02alfL1lvI') const jwt = await new jose.EncryptJWT({ 'urn:example:claim': true }) .setProtectedHeader({ alg: 'dir', enc: 'A128CBC-HS256' }) .setIssuedAt() .setIssuer('urn:example:issuer') .setAudience('urn:example:audience') .setExpirationTime('2h') .encrypt(secret) console.log(jwt)未加密场景(UnsecuredJWT 文档示例)则直接调用同步的encode():
const unsecuredJwt = new jose.UnsecuredJWT({ 'urn:example:claim': true }) .setIssuedAt() .setIssuer('urn:example:issuer') .setAudience('urn:example:audience') .setExpirationTime('2h') .encode()内部实现:WeakMap 隔离载荷
ProduceJWT的方法在底层由JWTClaimsBuilder实现(src/lib/jwt_claims_set.ts)。值得注意的实现细节是:构造器通过structuredClone(payload)深拷贝初始载荷,并将结果保存在一个WeakMap<object, JWTPayload>(producerPayloads)中,按生产者实例隔离数据。当setIssuedAt()未传参时写入的当前时间戳、setExpirationTime('2h')换算出的绝对时间戳,最终都进入该 WeakMap 对应的载荷对象。
终端方法(如SignJWT.sign)会调用jwtData(this)(src/lib/jwt_claims_set.ts)取出载荷并序列化。序列化前还会校验iat、nbf、exp三个时间 Claim 必须是有限数值,非有限数值(如Infinity)会抛出TypeError。
时间 Claim 的最终形态:绝对时间戳
需要特别强调的是:字符串时间跨度在写入时就被换算成绝对的 Unix 时间戳,而不是把'2h'原样写进载荷。以setExpirationTime('2h')为例,numericDate()(src/lib/jwt_claims_set.ts)执行epoch(new Date()) + secs('2h'),得到的exp是一个确定秒数。因此最终 JWT 的 payload 中所有时间 Claim 都是 RFC 7519 定义的 NumericDate(Unix 时间戳秒数),这也与 JWTPayload 接口 中nbf?: number、exp?: number、iat?: number的类型声明相互印证。
与消费端的呼应:验证选项对齐
ProduceJWT设置的标准 Claim,与消费端 JWTClaimVerificationOptions 中的校验选项一一对应:
- 生产端
setIssuer↔ 消费端issuer选项(期望签发者,设置后强制要求issClaim 存在); - 生产端
setAudience↔ 消费端audience选项(期望受众,设置后强制要求audClaim 存在); - 生产端
setSubject↔ 消费端subject选项(期望主体); - 生产端
setExpirationTime↔ 消费端exp校验(exp <= now - clockTolerance时抛出 JWTExpired); - 生产端
setNotBefore↔ 消费端nbf校验(nbf > now + clockTolerance时校验失败); - 生产端
setIssuedAt↔ 消费端maxTokenAge选项(设置后强制要求iatClaim 存在并检查 Token 年龄)。
并且验证侧同样复用了secs()来解析clockTolerance、maxTokenAge等字符串形式的选项(src/lib/jwt_claims_set.ts),保证"生产用 '2h',验证用 '10 minutes'"这类字符串语义在整条链路上完全一致。这种生产端与消费端的对称设计,是 jose 在处理 JWT 时间语义上的一致性保证。
使用建议
- 优先使用字符串时间跨度表达相对时间(如
'2h'、'-1 day'、'30 minutes from now'),可读性强且自动换算为绝对时间戳;需要精确控制时再直接传number时间戳。 setIssuedAt()建议无参调用,自动以当前时间为基准,配合消费端maxTokenAge可实现"Token 签发后 N 秒内有效"的滑动窗口策略。- 同一 Claim 只设置一次:
JWTClaimsBuilder的赋值是覆盖式写入,重复链式调用后值以最后一次为准,且SignJWT.setProtectedHeader等头部方法在重复调用时会通过assertNotSet抛错(src/jwt/sign.ts),因此应避免在链式表达式中重复设置同一属性。 - 结合泛型载荷使用:构造器接受的
payload为 JWTPayload,其中[propName: string]: unknown允许携带任意自定义 Claim,标准 Claim 则由上述七方法写入,二者互不冲突。
延伸阅读
- SignJWT 类文档:签名 JWT 的完整用法与密钥示例(PKCS#8、JWK)
- EncryptJWT 类文档:加密 JWT 的完整用法与
replicateIssuerAsHeader等复制方法 - UnsecuredJWT 类文档:
alg: none场景的编码与解码 - JWTPayload 接口:标准 Claim 的完整类型定义
- JWTClaimVerificationOptions 接口:消费端与
ProduceJWT一一对应的校验选项 - secs 单元测试:时间跨度解析的完整边界用例
- JWTClaimsBuilder 实现:
ProduceJWT契约的底层实现
- 网络安全
- 认证鉴权
- 后端
【免费下载链接】jose
JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes
相关推荐
LiveCodeBench leaderboard提交指南:如何让你的模型快速登上代码能力排行榜
LiveCodeBench leaderboard提交指南:如何让你的模型快速登上代码能力排行榜 LiveCodeBench是一个全面且无污染的代码大语言模型评
网络安全认证鉴权后端解密加密 JWT 的结果契约:jose 库 JWTDecryptResult 接口全解析
解密加密 JWT 的结果契约:jose 库 JWTDecryptResult 接口全解析 jose 是一个为 Node.js、浏览器、Cloudflare Wo
网络安全认证鉴权后端jose 中的 decodeJwt:免验签解析 JWT Claims Set 的完整指南
jose 中的 decodeJwt:免验签解析 JWT Claims Set 的完整指南 decodeJwt 是 jose 库中用于解析 JSON Web To
网络安全认证鉴权后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考