先说结论
如果你搜过「JWT 解码 」「JWT 在线验证 」「JWT 生成器 」「JWT PEM JWK 怎么用」,大概率不是想先看一堆抽象概念,而是想马上解决这几个问题:
- 这个 token 里到底写了什么?
- 为什么后端说签名不对?
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 字符串:
text
header.payload.signature
例如:
text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
└────────── header ──────────┘└────────── payload ─────────┘└──────── signature ────────┘
页面会马上把它拆开,并分别展示:
- Header
- Payload
- Signature
而且 Header、Payload 会自动做 Base64URL 解码 和 JSON 格式化。
2. 自动识别算法,并显示 token 是否过期
页面会从 Header 中读出 alg,比如:
json
{"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 必须有三段:
text
header.payload.signature
如果只有两段,或者多了空格、换行、前后缀,页面会直接提示格式错误。这一步能先排除很多复制粘贴问题。
第二步:看 Header 里的算法是不是你以为的那个
很多问题不是"签名算错了",而是算法根本不一致。
比如你以为后端在用:
json
{"alg":"RS256","typ":"JWT"}
结果 token 实际上是:
json
{"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 后,会把:
text
header.payload
重新按约定算法签一遍,再去和 token 里自带的 signature 比较:
- 一样:说明内容没被改过
- 不一样:说明 token 被伪造或密钥不匹配
一句话总结:
JWT 默认是"可读取但不可篡改",不是"谁都看不懂"。
这也是为什么 payload 里绝对不能放密码、身份证号、银行卡号这类敏感明文。
四、页面实现思路:前端怎么把 JWT 工作台做完整
下面这部分才是和页面最贴合的内容。不是泛泛聊 JWT,而是拆我们这个工具页为什么能工作。
整体流程图
flowchart LR A"用户输入 Token / 编辑 Header Payload" --> B"拆分三段" B --> C"Base64URL 解码 Header 和 Payload" C --> D"JSON 格式化展示" B --> E"读取 header.alg" E --> F"选择 HMAC / RSA / ECDSA / RSA-PSS" F --> G"导入 Secret / PEM / JWK" G --> H"Web Crypto 签名或验签" C --> I"解析 exp iat nbf 等 Claims" H --> J"显示验签结果" I --> K"显示过期时间与状态" J --> L"前端直接完成计算" K --> L
对应到页面能力,大致可以拆成 5 层:
- Token 解析层
- Base64URL 编解码层
- 算法与密钥导入层
- 签名 / 验签层
- UI 展示层
1. Token 解析层
核心逻辑其实很直接:先按 . 拆分,再分别解码 Header、Payload。
javascript
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:
+换成-/换成_- 去掉结尾
=
浏览器里可以这样处理:
javascript
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,而是要统一处理不同算法族:
- HMAC
- RSA
- ECDSA
- RSA-PSS
而且每种算法需要的密钥格式又不一样。
页面里比较实用的做法是先把算法抽象成配置对象:
javascript
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 的签名逻辑大致这样:
javascript
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 签发 + 验签
javascript
// 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
html
<!doctype html>
<html>
<body>
<pre id="out"></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 / secret
2. 不要相信客户端传来的 alg
错误示例:
javascript
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 家族,应该能通用"。实际上不能。
RS256:RSASSA-PKCS1-v1_5PS256:RSA-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 的流程不只停留在"解码看看",通常还会连着处理这些步骤:
- Base64 / Base64URL 编解码
- HMAC 生成与验证
- Authorization Header 构造
- HTTP 请求调试
这些动作本来就经常会连在一起出现,所以实现上也不应该只做单点解析,而是尽量把整条排障链路补完整。
如果你正好在找一个能直接上手的在线 JWT 工具,可以直接试试这个页面:
适合平时调试 token、排查鉴权、验证签名和快速生成测试 JWT。