☰
jose 中的 UnsecuredJWT:使用 `{ “alg“: “none“ }` 无签名 JWT 的编码、解码与校验指南
2026/9/27 10:09:00 网站建设 项目流程
  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】jose

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

项目地址:https://gitcode.com/gh_mirrors/jo/jose
点击查看免费下载

导读

UnsecuredJWT是 jose 库中用于处理Unsecured(无签名、无加密)JWT的工具类,这类 JWT 的受保护头部固定为{ "alg": "none" },通常用于纯声明传递、临时票据、内部调试或对安全要求极低的应用场景。本文将以官方文档 docs/jwt/unsecured/classes/UnsecuredJWT.md 为骨架,结合仓库源码 src/jwt/unsecured.ts 与测试用例,完整讲解该类从构造、声明设置、编码到解码验证的全流程,并深入剖析其底层校验逻辑、时间跨度解析规则与典型错误场景。读完本文,你将能够熟练使用UnsecuredJWT完成无签名 JWT 的生产与消费,并理解其与常规签名 JWT 在安全性上的本质差异。

一、UnsecuredJWT 是什么

根据 jose 官方 API 文档的定义,UnsecuredJWT类是一个用于处理{ "alg": "none" }Unsecured JWT 的实用工具。所谓 "Unsecured",即令牌既不签名也不加密:它的三段式紧凑序列中,第三段(签名段)为空字符串,整体结构为header.payload.(注意末尾的点号后没有任何签名内容)。

该类的定位可以从源码的模块注释中得到印证(见 src/jwt/unsecured.ts):

Unsecured (unsigned & unencrypted) JSON Web Tokens (JWT)

导出方式

UnsecuredJWT以命名导出的方式从主模块入口'jose'导出,同时也从子路径导出'jose/jwt/unsecured'提供。在仓库 src/index.ts 中可以看到这两行导出声明:

export { UnsecuredJWT } from './jwt/unsecured.js' export type { UnsecuredResult } from './jwt/unsecured.js'

因此你可以用以下任一方式引入:

import { UnsecuredJWT } from 'jose' // 或 import { UnsecuredJWT } from 'jose/jwt/unsecured'

适用场景与安全警告

无签名 JWT 意味着任何拿到令牌的人都可以随意篡改 payload 并重新编码,它不提供任何完整性保护。因此它只适合:

  • 传递无需防篡改的公开声明(例如urn:example:claim: true这样的只读标记);
  • 作为示例、教学或调试过程中的临时令牌;
  • 下游系统已通过其他通道(如 TLS 之外的业务上下文)确认可信的场景。

切勿用它承载敏感数据、身份认证或授权决策。jose 官方文档将此类令牌明确定义为 "Unsecured",正是为了强调这一点。

二、编码(Encode):构造与声明链式设置

构造函数

new UnsecuredJWT(payload?)
  • payload?:JWTPayload类型,即 JWT Claims Set 对象,默认值为空对象{}。

源码中,构造函数实际继承自JWTClaimsBuilder(见 src/jwt/unsecured.ts),其实现位于 src/lib/jwt_claims_set.ts:

export class JWTClaimsBuilder { constructor(payload: types.JWTPayload = {}) { if (!isObject(payload)) { throw new TypeError('JWT Claims Set MUST be an object') } ;(producerPayloads ||= new WeakMap()).set(this, structuredClone(payload)) } // ... }

需要注意两点实现细节:

  1. 传入的 payload 必须是普通对象,否则抛出TypeError('JWT Claims Set MUST be an object');
  2. payload 会通过structuredClone进行深拷贝存入WeakMap,之后再调用链式 setter 修改的是副本,不会污染调用方传入的原始对象。

编码方法encode()

encode(): string

encode()将当前 Claims Set 编码为紧凑形式的 Unsecured JWT 字符串。源码(src/jwt/unsecured.ts)非常直白:

encode(): string { const header = b64u.encode(JSON.stringify({ alg: 'none' })) const payload = b64u.encode(jwtData(this)) return `${header}.${payload}.` }

即:头部固定为{ "alg": "none" },与 payload 分别做 base64url 编码,用.拼接,最后签名段为空。jwtData(见 src/lib/jwt_claims_set.ts)在序列化前还会校验iat、nbf、exp三个时间声明若为数字则必须是有限数(Number.isFinite)。

完整的编码示例

官方文档给出的编码示例(已转换为从仓库根目录出发的完整上下文):

const unsecuredJwt = new jose.UnsecuredJWT({ 'urn:example:claim': true }) .setIssuedAt() .setIssuer('urn:example:issuer') .setAudience('urn:example:audience') .setExpirationTime('2h') .encode() console.log(unsecuredJwt)

在测试 test/jwt/unsecured.test.ts 中,new UnsecuredJWT({ 'urn:example:claim': true }).encode()的产物被固定为:

eyJhbGciOiJub25lIn0.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6dHJ1ZX0.

解码这三段可以看到:头部为{"alg":"none"},payload 为{"urn:example:claim":true},签名段为空。另外,new UnsecuredJWT().encode()(空 payload)的输出是eyJhbGciOiJub25lIn0.e30.,其中e30.正是空对象{}的 base64url 编码(见 test/jwt/unsecured.test.ts)。

链式声明设置方法

以下 setter 均返回this,支持链式调用。它们定义在JWTClaimsBuilder基类中(src/lib/jwt_claims_set.ts),每个方法都会先做类型校验再写入 payload。

方法对应 Claims参数类型校验规则
setIssuer(issuer)iss(Issuer)string非字符串抛TypeError
setSubject(subject)sub(Subject)string非字符串抛TypeError
setAudience(audience)aud(Audience)string \| string[]必须是字符串或全为字符串的数组
setJti(jwtId)jti(JWT ID)string非字符串抛TypeError
setIssuedAt(input?)iat(Issued At)string \| number \| Date,可省略省略时取当前时间戳
setExpirationTime(input)exp(Expiration Time)string \| number \| Date见下文时间规则
setNotBefore(input)nbf(Not Before)string \| number \| Date见下文时间规则

对应的类型定义见JWTPayload:aud?、exp?、iat?、iss?、jti?、nbf?、sub?均为可选成员,且允许携带任意其他自定义成员。

时间声明(iat / exp / nbf)的输入规则

这是本类最值得细读的部分。三个时间 setter 接受三种输入(官方文档 UnsecuredJWT.md 有完整说明):

  1. number:直接作为 Unix 时间戳(秒)使用;
  2. Date:转换为 Unix 时间戳(Math.floor(date.getTime() / 1000));
  3. string:解析为相对当前 Unix 时间戳的时间跨度。

其中字符串的解析实现位于 src/lib/jwt_claims_set.ts,核心正则与换算如下:

const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i const multipliers = { s: 1, m: 60, h: 3600, d: 86400, w: 604800, y: 31557600 }

关键规则总结:

  • 格式:数字后跟单位,例如"5 minutes"、"1 day";
  • 合法单位拼写:sec/secs/second/seconds/s;minute/minutes/min/mins/m;hour/hours/hr/hrs/h;day/days/d;week/weeks/w;year/years/yr/yrs/y(正则忽略大小写,且只取首字符映射到乘数);
  • 月份不受支持;
  • 一年按 365.25 天计算(乘数y: 31557600= 365.25 × 86400);
  • 减法:前置-(如-10s)或后缀"ago"(如"10s ago")都会让结果取负,即时间往过去偏移;
  • "from now" 后缀:"10s from now"仅用于可读性,语义就是加到当前时间戳上;前置+(如+10s)同样合法;
  • 单位缺省(如"10")或月份(month)会抛出TypeError('Invalid time period format')。

numericDate的统一换算逻辑(src/lib/jwt_claims_set.ts)为:

function numericDate(value: number | string | Date, label: string) { if (typeof value === 'number') return validateInput(label, value) if (value instanceof Date) return validateInput(label, epoch(value)) return epoch(new Date()) + secs(value) }

即字符串输入最终都是当前Unix时间戳 + secs(字符串)。测试 test/jwt/time_setters.ts 覆盖了'10s'、'+10s'、'-10s'、'+ 10s'、'- 10s'、'10s from now'、'10s ago'、new Date(now * 1000)、数字0等多种输入向量,并通过 test/jwt/unsecured.test.ts 对setIssuer、setSubject、setAudience、setJti、setIssuedAt、setExpirationTime、setNotBefore七个方法逐一断言其写入的声明值与期望值一致。

setIssuedAt()还有一个特例:不传参数时直接使用当前时间戳(见 src/lib/jwt_claims_set.ts):

setIssuedAt(value?: number | string | Date): this { const payload = producerPayload(this) if (value === undefined) { payload.iat = epoch(new Date()) } else if (typeof value === 'string') { payload.iat = validateInput('setIssuedAt', epoch(new Date()) + secs(value)) } else { payload.iat = numericDate(value, 'setIssuedAt') } return this }

三、解码(Decode):静态方法与 Claims 校验

静态方法签名

static decode<PayloadType = JWTPayload>(jwt: string, options?: JWTClaimVerificationOptions): UnsecuredResult<PayloadType>
  • jwt:要解码的 Unsecured JWT 字符串;
  • options?:JWTClaimVerificationOptions,JWT Claims Set 验证选项;
  • 泛型PayloadType默认JWTPayload,用于声明你期望 payload 携带的类型;
  • 返回值:UnsecuredResult,包含payload(JWT Claims Set)与header(JOSE 头部,对 Unsecured JWT 恒为{ "alg": "none" })。

官方文档的解码示例:

const { payload, header } = jose.UnsecuredJWT.decode(unsecuredJwt, { issuer: 'urn:example:issuer', audience: 'urn:example:audience', }) console.log(header) console.log(payload)

解码底层实现与防御性校验

decode的实现位于 src/jwt/unsecured.ts,其校验链条非常严密,共分五步:

第一步:入参类型检查

if (typeof jwt !== 'string') { throw new JWTInvalid('Unsecured JWT must be a string') }

第二步:紧凑结构检查

const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split('.') if (length !== 3 || signature !== '') { throw new JWTInvalid('Invalid Unsecured JWT') }

必须是三段且第三段为空字符串。测试中的'....'、'..'、'..foo'以及带签名段的'eyJhbGciOiJIUzI1NiJ9...'都会命中此分支(见 test/jwt/unsecured.test.ts)。

第三步:头部解析与 JWS 选项校验

头部经parseJoseHeader解析后,会依次执行validateCrit(crit 扩展参数识别)与validateB64(b64 参数校验)。任何JWSInvalid都会被包装为JWTInvalid('Invalid Unsecured JWT', { cause })抛出,因此调用方看到的是统一错误码ERR_JWT_INVALID,且可通过error.cause查看底层的ERR_JWS_INVALID详情。测试 test/jwt/unsecured.test.ts 覆盖了crit为 null、crit缺少对应扩展参数、b64类型错误等多种头部畸形场景。

第四步:算法与编码方式硬约束

if (header.alg !== 'none') { throw new JWTInvalid('Invalid Unsecured JWT') } if (!b64) { throw new JWTInvalid('JWTs MUST NOT use unencoded payload') }
  • 头部alg必须是'none',否则拒绝解码(如测试中传入alg: 'HS256'的令牌);
  • 禁止使用b64: false的未编码 payload 形式——这正是 RFC 7797);
  • 同时,带有未被识别的crit扩展头参数的令牌会被拒绝并抛出ERR_JOSE_NOT_SUPPORTED(见 test/jwt/unsecured.test.ts)。

第五步:Claims Set 解析与选项验证

payload 段先 base64url 解码(失败会抛JWTInvalid('Failed to base64url decode the payload')),随后交给validateClaimsSet(src/lib/jwt_claims_set.ts)执行完整的 Claims 验证。payload 必须是顶层 JSON 对象,否则抛JWTInvalid('JWT Claims Set must be a top-level JSON object')。

验证选项详解(JWTClaimVerificationOptions)

decode的第二个参数即JWTClaimVerificationOptions,所有字段均为可选:

选项类型作用
issuerstring \| string[]期望的iss值;设置后强制要求iss声明存在
subjectstring期望的sub值;设置后强制要求sub声明存在
audiencestring \| string[]期望的aud值(可多个);设置后强制要求aud声明存在
requiredClaimsstring[]必须存在的额外声明名列表
maxTokenAgestring \| number从iat起算的最大存活时间(秒或时间跨度字符串);设置后强制要求iat存在
clockTolerancestring \| number时钟偏移容忍(秒或时间跨度字符串),用于nbf、exp及maxTokenAge下的iat比较
currentDateDate比较 NumericDate 时使用的"当前时间",默认new Date()
typstring期望的typ头部参数值;设置后强制要求头部携带typ

底层实现要点(src/lib/jwt_claims_set.ts):

  • 存在性检查:requiredClaims加上由maxTokenAge/audience/subject/issuer选项隐式引入的iat/aud/sub/iss,构成必须存在的声明集合,缺失即抛JWTClaimValidationFailed(missing原因);
  • 值匹配:iss支持数组任一匹配,aud支持"payload 的 aud 数组包含任一期望值"的包含式匹配(checkAudiencePresence见 src/lib/jwt_claims_set.ts),sub要求严格相等,不匹配抛check_failed原因;
  • typ归一化:比较时会把typ小写化,并允许"JWT"与"application/JWT"互相等价(normalizeTyp,src/lib/jwt_claims_set.ts);
  • nbf:nbf > now + tolerance时抛JWTClaimValidationFailed;
  • exp:exp <= now - tolerance时抛JWTExpired(即ERR_JWT_EXPIRED);
  • maxTokenAge:now - iat - tolerance > max抛JWTExpired(iat过旧);now - iat < -tolerance抛JWTClaimValidationFailed(iat在未来);
  • 三个时间声明(iat/nbf/exp)若存在但不是数字,会以invalid原因抛JWTClaimValidationFailed。

相关错误类型都定义在 src/util/errors.ts:JWTInvalid(第 418 行起)、JWTExpired(第 196 行起)、JWTClaimValidationFailed(第 135 行起),它们统一继承自JOSEError基类,可通过error.code获取形如ERR_JWT_INVALID、ERR_JWT_EXPIRED、ERR_JWT_CLAIM_VALIDATION_FAILED的错误码,便于上层做分类处理。

类型化解码

由于decode是泛型方法,你可以为 payload 声明具体类型以获取完整的 TypeScript 推导:

interface MyClaims { 'urn:example:claim': boolean sub?: string } const { payload, header } = UnsecuredJWT.decode<MyClaims>(token, { issuer: 'urn:example:issuer', }) // payload 的类型为 MyClaims & JWTPayload

解码结果UnsecuredResult<PayloadType>的完整结构见 docs/jwt/unsecured/interfaces/UnsecuredResult.md:payload为PayloadType & JWTPayload的交叉类型,header恒为JWSHeaderParameters(对 Unsecured JWT 总是{ "alg": "none" })。

四、测试覆盖:从测试用例反推行为契约

仓库在 test/jwt/unsecured.test.ts 中对本类做了系统性的行为验证,除前文已引用的场景外,还包括:

  • 空 payload 编码:new UnsecuredJWT().encode()产出eyJhbGciOiJub25lIn0.e30.;
  • 非法输入矩阵:null、'....'、'..'、'..foo'、非none算法的令牌、非 base64url 的 payload 段,全部断言抛出ERR_JWT_INVALID及对应错误消息;
  • crit 与 b64 扩展:拒绝未知 crit 扩展、非法 crit 结构、非法 b64 类型;同时允许b64: false/b64: true/b64: 'false'这些在非 JWT 上下文中合法、但在 Unsecured JWT 中会被安全处理的头部变体(注意:只要b64不为显式false即视为编码正常);
  • 七个链式 setter 的完整向量测试:通过 test/jwt/time_setters.ts 提供的输入矩阵逐一验证写入正确性。

这些测试证明了UnsecuredJWT的一个核心设计态度:对任何不符合 Unsecured JWT 严格定义的输入一律拒绝,绝不静默降级。

五、与其他模块的对照:何时不该使用 UnsecuredJWT

jose 仓库中与之形成对照的是签名与加密路径:

  • 需要完整性保护时,应改用CompactSign+compactVerify(或 Flattened / General 变体),并通过jwtVerify校验签名后消费 JWT;
  • 需要机密性保护时,应使用EncryptJWT+jwtDecrypt;
  • UnsecuredJWT只应在"无篡改风险"这一前提明确成立时使用。文档中反复出现的{ "alg": "none" }定义,正是 JOSE 规范对这类令牌的标准化称呼。

六、完整实战示例

将编码与解码串联起来,一个完整的可运行示例(Node.js / Deno / Bun 等支持 Web API 的环境均可):

import { UnsecuredJWT } from 'jose' // —— 编码 —— const token = new UnsecuredJWT({ 'urn:example:claim': true }) .setIssuedAt() .setIssuer('urn:example:issuer') .setAudience(['urn:example:audience-a', 'urn:example:audience-b']) .setExpirationTime('2h') .setNotBefore('-5 minutes') // 允许最多 5 分钟的签发时钟偏移 .setJti('550e8400-e29b-41d4-a716-446655440000') .encode() console.log(token) // header.payload.(无签名段) // —— 解码 + 校验 —— try { const { payload, header } = UnsecuredJWT.decode(token, { issuer: 'urn:example:issuer', audience: ['urn:example:audience-a', 'urn:example:audience-b'], clockTolerance: '5 minutes', // 容忍签发方与消费方之间的时钟偏移 }) console.log(header) // { alg: 'none' } console.log(payload) // 包含 iat / iss / aud / exp / nbf / jti 及自定义声明 } catch (err) { if (err.code === 'ERR_JWT_EXPIRED') { console.error('令牌已过期') } else if (err.code === 'ERR_JWT_CLAIM_VALIDATION_FAILED') { console.error('Claims 校验失败', err.claim, err.reason) } else { console.error('无效的 Unsecured JWT', err.code, err.message) } }

七、小结与最佳实践

UnsecuredJWT将{ "alg": "none" }无签名 JWT 的编码与解码封装为一组类型安全、校验严密的 API。使用时的最佳实践建议:

  1. 明确安全边界:仅在无篡改风险或完整性由外部机制保障的场景使用;
  2. 充分利用验证选项:解码时传issuer、audience、requiredClaims、maxTokenAge、clockTolerance等选项,把 Claims 校验交给 jose,而不是手工拼 JSON;
  3. 善用错误码:基于ERR_JWT_INVALID、ERR_JWT_EXPIRED、ERR_JWT_CLAIM_VALIDATION_FAILED等错误码做分类处理,并通过error.cause定位底层ERR_JWS_INVALID细节;
  4. 时间跨度字符串要精确:牢记单位集合(秒/分/时/天/周/年)、ago与-表示过去、年按 365.25 天换算,且不支持月。

如需深入了解相关类型与选项,可继续阅读仓库中的 docs/jwt/unsecured/interfaces/UnsecuredResult.md、docs/types/interfaces/JWTClaimVerificationOptions.md 与 docs/types/interfaces/JWTPayload.md;实现细节可对照 src/jwt/unsecured.ts 与 src/lib/jwt_claims_set.ts,测试契约可参考 test/jwt/unsecured.test.ts。

  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】jose

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

项目地址:https://gitcode.com/gh_mirrors/jo/jose
点击查看免费下载

相关推荐

上一篇:终极免费OCR神器:Umi-OCR离线文字识别完全指南
下一篇:OpenCore Legacy Patcher终极指南:让老Mac免费运行最新macOS系统的完整解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询