为什么后端说签名不对HS256、RS256、ES256、PS256到底怎么切公钥私钥是PEM还是JWK到底该贴哪种改了 payload 之后怎么重新生成一个能用的 JWT所以这篇不只讲 JWT 原理我会直接结合这个实际的页面实现来拆页面实际能做到什么一次完整的 JWT 排查流程怎么走前端页面是怎么实现解码、验签、生成的哪些代码你可以直接 copy 过去自己用。这页工具不是“只能 decode 一下”的简单版而是一个JWT 工作台支持decode / encode双模式支持HS256 / HS384 / HS512 / RS256 / RS384 / RS512 / ES256 / ES384 / ES512 / PS256 / PS384 / PS512支持PEM / JWK两种密钥格式支持自动生成RSA / ECDSA / RSA-PSS密钥对支持实时验签、过期判断、Claims 语义化展示基于浏览器原生能力完成解码、签名和验签不需要额外起一个后端中转服务。一、先看效果这个 JWT 工具到底能做什么1. 粘贴一个 token立刻拆成三段JWT 本质上就是三段Base64URL字符串header.payload.signature例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 └────────── header ──────────┘└────────── payload ─────────┘└──────── signature ────────┘页面会马上把它拆开并分别展示HeaderPayloadSignature而且Header、Payload会自动做Base64URL 解码和JSON 格式化。2. 自动识别算法并显示 token 是否过期页面会从 Header 中读出alg比如{alg:HS256,typ:JWT}然后识别当前 token 使用的是哪种算法读取exp / iat / nbf这些标准 Claims自动把 Unix 时间戳转换成可读时间如果exp已经过期直接高亮提示。这一步对接口联调很有用。很多时候你以为是签名错了结果只是token 过期了。3. 直接输入密钥实时验证签名这页真正有价值的地方不是“能看 payload”而是能验签。不同算法对应不同输入HS256 / HS384 / HS512输入shared secretRS256 / RS384 / RS512 / PS256 / PS384 / PS512输入公钥ES256 / ES384 / ES512输入公钥而且密钥格式既支持PEMJWK输入正确后页面会实时显示签名验证通过签名验证失败密钥格式错误4. 不只是解码还能直接生成新的 JWT切到encode模式后你可以直接改 Header JSON改 Payload JSON选择算法填 Secret 或 Private Key实时生成新的 token这对于下面这些场景非常实用本地模拟登录态测试网关 / API 鉴权复现线上 bug验证第三方系统发来的 JWT 格式5. 基于浏览器原生能力直接完成计算这页实现上比较实用的一点是直接使用浏览器原生Web Crypto API做签名和验签而不是再额外走一层后端服务。前端页面自己就能把解码、验签、生成这条链路跑通不需要为了一个调试页再补一层服务端接口算法切换、密钥格式切换都能实时反馈结果。二、一个真实排障流程为什么这个 JWT 验签失败如果你是后端、前端、测试最常见的问题通常不是“JWT 是什么”而是我手里明明有 token为什么服务端还是说 unauthorized这时可以用这页工具按下面顺序排。第一步先确认 JWT 格式是不是完整一个合法 JWT 必须有三段header.payload.signature如果只有两段或者多了空格、换行、前后缀页面会直接提示格式错误。这一步能先排除很多复制粘贴问题。第二步看 Header 里的算法是不是你以为的那个很多问题不是“签名算错了”而是算法根本不一致。比如你以为后端在用{alg:RS256,typ:JWT}结果 token 实际上是{alg:PS256,typ:JWT}RS256和PS256看起来只差两个字母但它们不是同一个签名方案直接混用一定验不过。第三步确认你喂进去的是对的密钥这里是排障最常见的坑HS256需要的是secret不是公钥RS256 / ES256 / PS256验签时需要的是public key不是private key某些系统给你的是JWK不是PEM某些 HMAC secret 实际是base64 编码后的字节不是普通字符串这页工具里专门做了两件事来降低误用提供PEM / JWK切换HMAC 提供base64 encoded开关。这两个开关很小但对真实联调非常关键。第四步看是不是过期不要误判成签名错误很多同学第一反应是“验签失败”但实际上问题可能是exp已过期nbf还没到生效时间服务端时钟和客户端时钟有偏差页面会把exp转成可读时间并高亮“已过期 / 未过期”这个细节能少走很多弯路。第五步如果 payload 改过必须重新签名JWT 默认不是加密而是签名。这意味着Header、Payload都能被任何人读出来但只要你改了其中任意一个字节原来的Signature就失效。所以正确流程不是“我改完 payload 再拿原 token 去请求”而是修改 Header / Payload用正确算法和密钥重新签名拿新生成的 token 去请求这也是为什么页面里一定要同时提供decode和encode两个模式。只做解码排障链路是不完整的。三、JWT 到底是什么用「防伪火车票」讲明白现在再回来看 JWT 原理就会更容易理解。很多人第一次接触 JWT会误以为它是一段“加密后的字符串”。其实不准确。JWT 更像一张带防伪码的车票JWT火车票header票种说明payload乘客、车次、座位、发车时间signature钢印 / 防伪码JWT 不是为了“防偷看”而是为了“防伪造”。也就是说Header 和 Payload 通常都能被解码看到Signature 的作用是证明前两段没有被篡改。服务端收到 token 后会把header.payload重新按约定算法签一遍再去和 token 里自带的signature比较一样说明内容没被改过不一样说明 token 被伪造或密钥不匹配一句话总结JWT 默认是“可读取但不可篡改”不是“谁都看不懂”。这也是为什么 payload 里绝对不能放密码、身份证号、银行卡号这类敏感明文。四、页面实现思路前端怎么把 JWT 工作台做完整下面这部分才是和页面最贴合的内容。不是泛泛聊 JWT而是拆我们这个工具页为什么能工作。整体流程图用户输入 Token / 编辑 Header Payload拆分三段Base64URL 解码 Header 和 PayloadJSON 格式化展示读取 header.alg选择 HMAC / RSA / ECDSA / RSA-PSS导入 Secret / PEM / JWKWeb Crypto 签名或验签解析 exp iat nbf 等 Claims显示验签结果显示过期时间与状态前端直接完成计算对应到页面能力大致可以拆成 5 层Token 解析层Base64URL 编解码层算法与密钥导入层签名 / 验签层UI 展示层1. Token 解析层核心逻辑其实很直接先按.拆分再分别解码 Header、Payload。function parseJwt(token) { const parts token.trim().split(.) if (parts.length ! 3) throw new Error(JWT 格式错误) const header JSON.parse(b64UrlDecode(parts[0])) const payload JSON.parse(b64UrlDecode(parts[1])) return { header: parts[0], payload: parts[1], signature: parts[2], headerObj: header, payloadObj: payload, } }这个阶段只做三件事格式校验Base64URL 解码JSON 解析如果这里报错后面所有流程都不用继续。2. Base64URL 编解码层JWT 用的不是普通 Base64而是 Base64URL换成-/换成_去掉结尾浏览器里可以这样处理function b64UrlDecode(str) { let s str.replace(/-/g, ).replace(/_/g, /) while (s.length % 4) s return decodeURIComponent(escape(atob(s))) } function b64UrlEncodeStr(str) { return btoa(unescape(encodeURIComponent(str))) .replace(/\/g, -) .replace(/\//g, _) .replace(/$/, ) }别小看这层。JWT 联调里非常多“明明看起来一样但验签失败”的问题本质就是base64和base64url混了。3. 算法与密钥导入层页面真正麻烦的地方不是 decode而是要统一处理不同算法族HMACRSAECDSARSA-PSS而且每种算法需要的密钥格式又不一样。页面里比较实用的做法是先把算法抽象成配置对象const ALGORITHMS [ { value: HS256, type: hmac, hash: SHA-256 }, { value: RS256, type: rsa, hash: SHA-256 }, { value: ES256, type: ecdsa, hash: SHA-256, namedCurve: P-256 }, { value: PS256, type: rsapss, hash: SHA-256 }, ]这样后面导入密钥、签名、验签时就不需要写一堆零散if else。对于非对称算法还要同时兼容PEMJWK这一步很关键因为很多OAuth / OIDC / 网关系统给出来的就是JWK。4. 签名与验签层浏览器端我们直接用原生crypto.subtle。HMAC 的签名逻辑大致这样async function signHmac(data, secret) { const key await crypto.subtle.importKey( raw, new TextEncoder().encode(secret), { name: HMAC, hash: SHA-256 }, false, [sign] ) const sig await crypto.subtle.sign(HMAC, key, new TextEncoder().encode(data)) return b64UrlEncode(sig) }RSA / ECDSA / RSA-PSS 则先导入公私钥再调用不同参数的sign或verify。这里有两个实现细节很值得写进文章decode模式下非对称算法要用public key验签encode模式下非对称算法要用private key签名很多实现做不全就是卡在这里最后只能“看”不能“用”。5. Claims 展示层如果只是把 payload 原样打印出来其实还不够好用。更实用的做法是做一层“语义增强”exp标成“过期时间”iat标成“签发时间”nbf标成“生效时间”Unix 时间戳自动转人类可读时间过期状态直接高亮这部分不复杂但对排障体验很有帮助。用户不用自己再把时间戳复制出去换算也更容易第一眼看出问题到底出在签名、时间还是Claims。五、可以直接复制的核心代码如果你想把这个能力放进自己的项目里下面几段就够你起步。1. 50 行 Node.js手写 HS256 签发 验签// jwt-demo.js const crypto require(node:crypto) function b64url(input) { return Buffer.from(input) .toString(base64) .replace(/\/g, -) .replace(/\//g, _) .replace(/$/, ) } function hmacSign(data, secret) { return b64url(crypto.createHmac(sha256, secret).update(data).digest()) } function sign(payload, secret) { const header b64url(JSON.stringify({ alg: HS256, typ: JWT })) const body b64url(JSON.stringify(payload)) const signature hmacSign(${header}.${body}, secret) return ${header}.${body}.${signature} } function verify(token, secret) { const [h, p, s] token.split(.) const expected hmacSign(${h}.${p}, secret) const a Buffer.from(s) const b Buffer.from(expected) return a.length b.length crypto.timingSafeEqual(a, b) } const SECRET my-super-secret-key const payload { userId: 42, role: admin, exp: Math.floor(Date.now() / 1000) 3600 } const token sign(payload, SECRET) console.log(token) console.log(verify(token, SECRET)) // true console.log(verify(token, wrong-key)) // false这段代码的意义主要是把 JWT 最核心的机制讲明白Header Payload 先编码再对header.payload做签名。2. 浏览器端原生 Web Crypto 做 HMAC!doctype html html body pre idout/pre script const out document.getElementById(out) const b64url buf btoa(String.fromCharCode(...new Uint8Array(buf))) .replace(/\/g, -).replace(/\//g, _).replace(/$/, ) const b64urlStr str b64url(new TextEncoder().encode(str)) async function sign(payload, secret) { const header b64urlStr(JSON.stringify({ alg: HS256, typ: JWT })) const body b64urlStr(JSON.stringify(payload)) const data new TextEncoder().encode(${header}.${body}) const key await crypto.subtle.importKey( raw, new TextEncoder().encode(secret), { name: HMAC, hash: SHA-256 }, false, [sign] ) const sig await crypto.subtle.sign(HMAC, key, data) return ${header}.${body}.${b64url(sig)} } ;(async () { const token await sign({ user: alice, role: admin }, browser-demo-secret) out.textContent token })() /script /body /html这就是为什么这个页面可以直接在前端完成 HMAC 签名和验签而不用再补一个服务端调试接口。3. 为什么还要补非对称算法支持如果你只做HS256会发现很多真实用户其实用不上。因为很多现代认证系统、OIDC、第三方平台、企业网关默认给你的往往是RS256ES256PS256JWK所以实现上不能只停在HS256 decode而是要把非对称算法、密钥格式和验签流程一并补齐不然覆盖到的只是很小一部分真实场景。六、真实项目里最容易踩的 6 个坑1. JWT 不是加密是签名Payload 能读不代表不安全Payload 能被随便看才是默认状态。所以别把下面这些放进去密码身份证号银行卡号access key / secret2. 不要相信客户端传来的alg错误示例function badVerify(token) { const header JSON.parse(Buffer.from(token.split(.)[0], base64).toString()) return verifyByAlg(token, header.alg) }正确做法是服务端强制使用自己的配置算法而不是跟着 token 头部走。3.base64和base64url不能混JWT 一定是base64url不是普通base64。差别虽然只有几个字符但一旦混用签名必然不一致。4.RS256和PS256不是一回事很多人以为“都是 RSA 家族应该能通用”。实际上不能。RS256RSASSA-PKCS1-v1_5PS256RSA-PSS尤其RSA-PSS还要显式处理saltLength跨语言联调时很容易出问题。5.HMAC secret可能是原始字符串也可能是base64 字节这也是为什么页面里要有base64 encoded开关。很多用户不是不会验签而是把secret 的表达形式用错了。6. 过期判断是业务校验不只是签名校验签名通过只代表 token 没被改过不代表没过期已生效受众正确签发者可信所以真实服务端里验签只是第一步。七、为什么这种工具比纯解码器更好用很多 JWT 页面只能做一件事把 Payload 解出来给你看。但真实项目里光“看见内容”通常不够你还会继续碰到这些问题签名到底有没有通过算法是不是用错了这个 token 是不是已经过期改完 Payload 之后怎么重新生成公钥、私钥、Secret 到底该填哪个所以更完整的做法应该是把几个动作连起来先解码再验签再判断 Claims必要时重新生成这样才更接近日常联调和排障场景而不是只适合“看一眼内容”。八、最后给一句最实用的判断标准如果一个 JWT 页面只能把Payload解出来它最多算“查看器”。如果它还能切算法验签切PEM / JWK处理 HMAC secret 的不同输入形式自动生成非对称密钥对识别exp / iat / nbf前端直接完成计算那它才更接近真实项目里能反复用到的JWT 工作台。这也是我们这页想解决的核心问题不是只让你“看见” JWT而是让你把 JWT 调通。如果你平时调试 JWT 的流程不只停留在“解码看看”通常还会连着处理这些步骤
JWT 在线解码、验签、生成一篇讲透:附前端实现、工具架构与在线体验地址
为什么后端说签名不对HS256、RS256、ES256、PS256到底怎么切公钥私钥是PEM还是JWK到底该贴哪种改了 payload 之后怎么重新生成一个能用的 JWT所以这篇不只讲 JWT 原理我会直接结合这个实际的页面实现来拆页面实际能做到什么一次完整的 JWT 排查流程怎么走前端页面是怎么实现解码、验签、生成的哪些代码你可以直接 copy 过去自己用。这页工具不是“只能 decode 一下”的简单版而是一个JWT 工作台支持decode / encode双模式支持HS256 / HS384 / HS512 / RS256 / RS384 / RS512 / ES256 / ES384 / ES512 / PS256 / PS384 / PS512支持PEM / JWK两种密钥格式支持自动生成RSA / ECDSA / RSA-PSS密钥对支持实时验签、过期判断、Claims 语义化展示基于浏览器原生能力完成解码、签名和验签不需要额外起一个后端中转服务。一、先看效果这个 JWT 工具到底能做什么1. 粘贴一个 token立刻拆成三段JWT 本质上就是三段Base64URL字符串header.payload.signature例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 └────────── header ──────────┘└────────── payload ─────────┘└──────── signature ────────┘页面会马上把它拆开并分别展示HeaderPayloadSignature而且Header、Payload会自动做Base64URL 解码和JSON 格式化。2. 自动识别算法并显示 token 是否过期页面会从 Header 中读出alg比如{alg:HS256,typ:JWT}然后识别当前 token 使用的是哪种算法读取exp / iat / nbf这些标准 Claims自动把 Unix 时间戳转换成可读时间如果exp已经过期直接高亮提示。这一步对接口联调很有用。很多时候你以为是签名错了结果只是token 过期了。3. 直接输入密钥实时验证签名这页真正有价值的地方不是“能看 payload”而是能验签。不同算法对应不同输入HS256 / HS384 / HS512输入shared secretRS256 / RS384 / RS512 / PS256 / PS384 / PS512输入公钥ES256 / ES384 / ES512输入公钥而且密钥格式既支持PEMJWK输入正确后页面会实时显示签名验证通过签名验证失败密钥格式错误4. 不只是解码还能直接生成新的 JWT切到encode模式后你可以直接改 Header JSON改 Payload JSON选择算法填 Secret 或 Private Key实时生成新的 token这对于下面这些场景非常实用本地模拟登录态测试网关 / API 鉴权复现线上 bug验证第三方系统发来的 JWT 格式5. 基于浏览器原生能力直接完成计算这页实现上比较实用的一点是直接使用浏览器原生Web Crypto API做签名和验签而不是再额外走一层后端服务。前端页面自己就能把解码、验签、生成这条链路跑通不需要为了一个调试页再补一层服务端接口算法切换、密钥格式切换都能实时反馈结果。二、一个真实排障流程为什么这个 JWT 验签失败如果你是后端、前端、测试最常见的问题通常不是“JWT 是什么”而是我手里明明有 token为什么服务端还是说 unauthorized这时可以用这页工具按下面顺序排。第一步先确认 JWT 格式是不是完整一个合法 JWT 必须有三段header.payload.signature如果只有两段或者多了空格、换行、前后缀页面会直接提示格式错误。这一步能先排除很多复制粘贴问题。第二步看 Header 里的算法是不是你以为的那个很多问题不是“签名算错了”而是算法根本不一致。比如你以为后端在用{alg:RS256,typ:JWT}结果 token 实际上是{alg:PS256,typ:JWT}RS256和PS256看起来只差两个字母但它们不是同一个签名方案直接混用一定验不过。第三步确认你喂进去的是对的密钥这里是排障最常见的坑HS256需要的是secret不是公钥RS256 / ES256 / PS256验签时需要的是public key不是private key某些系统给你的是JWK不是PEM某些 HMAC secret 实际是base64 编码后的字节不是普通字符串这页工具里专门做了两件事来降低误用提供PEM / JWK切换HMAC 提供base64 encoded开关。这两个开关很小但对真实联调非常关键。第四步看是不是过期不要误判成签名错误很多同学第一反应是“验签失败”但实际上问题可能是exp已过期nbf还没到生效时间服务端时钟和客户端时钟有偏差页面会把exp转成可读时间并高亮“已过期 / 未过期”这个细节能少走很多弯路。第五步如果 payload 改过必须重新签名JWT 默认不是加密而是签名。这意味着Header、Payload都能被任何人读出来但只要你改了其中任意一个字节原来的Signature就失效。所以正确流程不是“我改完 payload 再拿原 token 去请求”而是修改 Header / Payload用正确算法和密钥重新签名拿新生成的 token 去请求这也是为什么页面里一定要同时提供decode和encode两个模式。只做解码排障链路是不完整的。三、JWT 到底是什么用「防伪火车票」讲明白现在再回来看 JWT 原理就会更容易理解。很多人第一次接触 JWT会误以为它是一段“加密后的字符串”。其实不准确。JWT 更像一张带防伪码的车票JWT火车票header票种说明payload乘客、车次、座位、发车时间signature钢印 / 防伪码JWT 不是为了“防偷看”而是为了“防伪造”。也就是说Header 和 Payload 通常都能被解码看到Signature 的作用是证明前两段没有被篡改。服务端收到 token 后会把header.payload重新按约定算法签一遍再去和 token 里自带的signature比较一样说明内容没被改过不一样说明 token 被伪造或密钥不匹配一句话总结JWT 默认是“可读取但不可篡改”不是“谁都看不懂”。这也是为什么 payload 里绝对不能放密码、身份证号、银行卡号这类敏感明文。四、页面实现思路前端怎么把 JWT 工作台做完整下面这部分才是和页面最贴合的内容。不是泛泛聊 JWT而是拆我们这个工具页为什么能工作。整体流程图用户输入 Token / 编辑 Header Payload拆分三段Base64URL 解码 Header 和 PayloadJSON 格式化展示读取 header.alg选择 HMAC / RSA / ECDSA / RSA-PSS导入 Secret / PEM / JWKWeb Crypto 签名或验签解析 exp iat nbf 等 Claims显示验签结果显示过期时间与状态前端直接完成计算对应到页面能力大致可以拆成 5 层Token 解析层Base64URL 编解码层算法与密钥导入层签名 / 验签层UI 展示层1. Token 解析层核心逻辑其实很直接先按.拆分再分别解码 Header、Payload。function parseJwt(token) { const parts token.trim().split(.) if (parts.length ! 3) throw new Error(JWT 格式错误) const header JSON.parse(b64UrlDecode(parts[0])) const payload JSON.parse(b64UrlDecode(parts[1])) return { header: parts[0], payload: parts[1], signature: parts[2], headerObj: header, payloadObj: payload, } }这个阶段只做三件事格式校验Base64URL 解码JSON 解析如果这里报错后面所有流程都不用继续。2. Base64URL 编解码层JWT 用的不是普通 Base64而是 Base64URL换成-/换成_去掉结尾浏览器里可以这样处理function b64UrlDecode(str) { let s str.replace(/-/g, ).replace(/_/g, /) while (s.length % 4) s return decodeURIComponent(escape(atob(s))) } function b64UrlEncodeStr(str) { return btoa(unescape(encodeURIComponent(str))) .replace(/\/g, -) .replace(/\//g, _) .replace(/$/, ) }别小看这层。JWT 联调里非常多“明明看起来一样但验签失败”的问题本质就是base64和base64url混了。3. 算法与密钥导入层页面真正麻烦的地方不是 decode而是要统一处理不同算法族HMACRSAECDSARSA-PSS而且每种算法需要的密钥格式又不一样。页面里比较实用的做法是先把算法抽象成配置对象const ALGORITHMS [ { value: HS256, type: hmac, hash: SHA-256 }, { value: RS256, type: rsa, hash: SHA-256 }, { value: ES256, type: ecdsa, hash: SHA-256, namedCurve: P-256 }, { value: PS256, type: rsapss, hash: SHA-256 }, ]这样后面导入密钥、签名、验签时就不需要写一堆零散if else。对于非对称算法还要同时兼容PEMJWK这一步很关键因为很多OAuth / OIDC / 网关系统给出来的就是JWK。4. 签名与验签层浏览器端我们直接用原生crypto.subtle。HMAC 的签名逻辑大致这样async function signHmac(data, secret) { const key await crypto.subtle.importKey( raw, new TextEncoder().encode(secret), { name: HMAC, hash: SHA-256 }, false, [sign] ) const sig await crypto.subtle.sign(HMAC, key, new TextEncoder().encode(data)) return b64UrlEncode(sig) }RSA / ECDSA / RSA-PSS 则先导入公私钥再调用不同参数的sign或verify。这里有两个实现细节很值得写进文章decode模式下非对称算法要用public key验签encode模式下非对称算法要用private key签名很多实现做不全就是卡在这里最后只能“看”不能“用”。5. Claims 展示层如果只是把 payload 原样打印出来其实还不够好用。更实用的做法是做一层“语义增强”exp标成“过期时间”iat标成“签发时间”nbf标成“生效时间”Unix 时间戳自动转人类可读时间过期状态直接高亮这部分不复杂但对排障体验很有帮助。用户不用自己再把时间戳复制出去换算也更容易第一眼看出问题到底出在签名、时间还是Claims。五、可以直接复制的核心代码如果你想把这个能力放进自己的项目里下面几段就够你起步。1. 50 行 Node.js手写 HS256 签发 验签// jwt-demo.js const crypto require(node:crypto) function b64url(input) { return Buffer.from(input) .toString(base64) .replace(/\/g, -) .replace(/\//g, _) .replace(/$/, ) } function hmacSign(data, secret) { return b64url(crypto.createHmac(sha256, secret).update(data).digest()) } function sign(payload, secret) { const header b64url(JSON.stringify({ alg: HS256, typ: JWT })) const body b64url(JSON.stringify(payload)) const signature hmacSign(${header}.${body}, secret) return ${header}.${body}.${signature} } function verify(token, secret) { const [h, p, s] token.split(.) const expected hmacSign(${h}.${p}, secret) const a Buffer.from(s) const b Buffer.from(expected) return a.length b.length crypto.timingSafeEqual(a, b) } const SECRET my-super-secret-key const payload { userId: 42, role: admin, exp: Math.floor(Date.now() / 1000) 3600 } const token sign(payload, SECRET) console.log(token) console.log(verify(token, SECRET)) // true console.log(verify(token, wrong-key)) // false这段代码的意义主要是把 JWT 最核心的机制讲明白Header Payload 先编码再对header.payload做签名。2. 浏览器端原生 Web Crypto 做 HMAC!doctype html html body pre idout/pre script const out document.getElementById(out) const b64url buf btoa(String.fromCharCode(...new Uint8Array(buf))) .replace(/\/g, -).replace(/\//g, _).replace(/$/, ) const b64urlStr str b64url(new TextEncoder().encode(str)) async function sign(payload, secret) { const header b64urlStr(JSON.stringify({ alg: HS256, typ: JWT })) const body b64urlStr(JSON.stringify(payload)) const data new TextEncoder().encode(${header}.${body}) const key await crypto.subtle.importKey( raw, new TextEncoder().encode(secret), { name: HMAC, hash: SHA-256 }, false, [sign] ) const sig await crypto.subtle.sign(HMAC, key, data) return ${header}.${body}.${b64url(sig)} } ;(async () { const token await sign({ user: alice, role: admin }, browser-demo-secret) out.textContent token })() /script /body /html这就是为什么这个页面可以直接在前端完成 HMAC 签名和验签而不用再补一个服务端调试接口。3. 为什么还要补非对称算法支持如果你只做HS256会发现很多真实用户其实用不上。因为很多现代认证系统、OIDC、第三方平台、企业网关默认给你的往往是RS256ES256PS256JWK所以实现上不能只停在HS256 decode而是要把非对称算法、密钥格式和验签流程一并补齐不然覆盖到的只是很小一部分真实场景。六、真实项目里最容易踩的 6 个坑1. JWT 不是加密是签名Payload 能读不代表不安全Payload 能被随便看才是默认状态。所以别把下面这些放进去密码身份证号银行卡号access key / secret2. 不要相信客户端传来的alg错误示例function badVerify(token) { const header JSON.parse(Buffer.from(token.split(.)[0], base64).toString()) return verifyByAlg(token, header.alg) }正确做法是服务端强制使用自己的配置算法而不是跟着 token 头部走。3.base64和base64url不能混JWT 一定是base64url不是普通base64。差别虽然只有几个字符但一旦混用签名必然不一致。4.RS256和PS256不是一回事很多人以为“都是 RSA 家族应该能通用”。实际上不能。RS256RSASSA-PKCS1-v1_5PS256RSA-PSS尤其RSA-PSS还要显式处理saltLength跨语言联调时很容易出问题。5.HMAC secret可能是原始字符串也可能是base64 字节这也是为什么页面里要有base64 encoded开关。很多用户不是不会验签而是把secret 的表达形式用错了。6. 过期判断是业务校验不只是签名校验签名通过只代表 token 没被改过不代表没过期已生效受众正确签发者可信所以真实服务端里验签只是第一步。七、为什么这种工具比纯解码器更好用很多 JWT 页面只能做一件事把 Payload 解出来给你看。但真实项目里光“看见内容”通常不够你还会继续碰到这些问题签名到底有没有通过算法是不是用错了这个 token 是不是已经过期改完 Payload 之后怎么重新生成公钥、私钥、Secret 到底该填哪个所以更完整的做法应该是把几个动作连起来先解码再验签再判断 Claims必要时重新生成这样才更接近日常联调和排障场景而不是只适合“看一眼内容”。八、最后给一句最实用的判断标准如果一个 JWT 页面只能把Payload解出来它最多算“查看器”。如果它还能切算法验签切PEM / JWK处理 HMAC secret 的不同输入形式自动生成非对称密钥对识别exp / iat / nbf前端直接完成计算那它才更接近真实项目里能反复用到的JWT 工作台。这也是我们这页想解决的核心问题不是只让你“看见” JWT而是让你把 JWT 调通。如果你平时调试 JWT 的流程不只停留在“解码看看”通常还会连着处理这些步骤