← 返回博客首页

JWT Token 解析与验证完全指南

什么是 JWT?

JWT(JSON Web Token,RFC 7519)是一种开放标准,把一组声明(claims)打包成 JSON 对象,并用签名保证它在传输过程中没有被改过。注意这句话的边界:JWT 保证的是完整性,不是机密性。它让服务端不用查库也能确认「这个令牌是我签发的、内容没被动过」。

它最常见的用途是身份认证:用户登录后拿一枚 Access Token,之后每次请求带上它,服务端验签即可识别用户,无需会话存储。

它和传统 Session 的差异

维度 Session + Cookie JWT
服务端状态 需要存 session(内存/Redis) 无状态,令牌自包含
水平扩容 需要共享存储或粘性会话 直接加机器
跨域 / 跨服务 Cookie 域限制麻烦 放在 Authorization 头,天然跨域
撤销能力 服务端删掉即失效 默认不可撤销,需额外设计
单次请求开销 一次存储查询 一次签名验算(通常更快)
载荷大小 只有一个 session id 承载全部声明,请求头更大

结论不是「谁更好」,而是:需要即时吊销、强管控的内部系统,Session 更省心;多服务、跨端、要横向扩容的 API,JWT 更合适。

手工拆一枚 JWT

一个 JWT 由三段 Base64Url 字符串用点号连接:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

把它丢进 JWT 解析器 会立刻还原成三段 JSON;想理解原理,可以用 Base64 转换器 手动解前两段。

第一段:Header

{ "alg": "HS256", "typ": "JWT" }
字段 含义 常见值
alg 签名算法 HS256 / RS256 / ES256 / EdDSA / none
typ 令牌类型 JWT(嵌套场景可为 at+jwt
kid 密钥 ID 用于 JWKS 中定位公钥,多密钥轮换时必填
jku JWKS 地址 谨慎使用,见「攻击」一节
cty 内容类型 嵌套 JWT(JWE 内嵌 JWS)时用到

第二段:Payload

Payload 里是一条条声明,分三类:

注册声明(RFC 7519 预定义,全部可选但强烈建议用)

Claim 含义 说明
iss Issuer,签发者 建议用完整 URL,如 https://auth.example.com
sub Subject,主体 通常为用户 ID,全局唯一且永不复用
aud Audience,受众 可以是字符串或数组;校验方必须确认自己在其中
exp Expiration,过期时间 Unix 秒级时间戳
nbf Not Before,生效时间 早于此刻的令牌应被拒绝
iat Issued At,签发时间 用于计算令牌年龄
jti JWT ID,唯一标识 防重放、做黑名单的关键字段

公共声明:需要避免命名冲突,应在 IANA 注册或用 URI 命名空间,例如 https://oltool.net/claims/role

私有声明:收发双方自行约定,如 roletenant_id

⚠️ 时间戳单位是不是毫秒。用 Date.now() 直接填进去会得到一个 50 年后的过期时间——这是线上最常见的低级错误之一。

第三段:Signature

签名覆盖前两段,计算公式(HS256):

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

RS256 则是 RSASSA-PKCS1-v1_5(SHA256, signingInput),ES256 是 ECDSA(P-256, SHA-256)。你会发现一个反直觉的点:签名算法不参与加密,它只是对「前两段的字节流」做一次摘要运算,再把结果 Base64Url 追加到后面。验签就是用同样的输入重算一遍,比对结果。

它不加密:Base64Url ≠ 保密

这是每个刚接触 JWT 的人都会踩的坑。Base64Url 只是把二进制安全地塞进 URL 的编码方式,任何解码器都能还原。想验证很简单:复制 Payload 段,用 Base64 转换器 解一下。

可以放:用户 ID、角色、租户、过期时间、权限范围(scope)、会话标识。

绝不能放:密码、密钥、身份证号、银行卡、完整手机号、内部系统拓扑。

确实需要保密时:用 JWE(RFC 7516)。JWE 是加密令牌,结构为五段,与 JWS 是两套标准。但绝大多数场景的正解不是上 JWE,而是别把敏感数据塞进令牌——令牌的使命是证明身份,不是当数据库用。

签名算法怎么选

算法 类型 密钥长度建议 签名长度 适用场景
HS256 对称 HMAC ≥ 256 bit 随机 43 B 单服务签发并校验
HS384 / HS512 对称 HMAC ≥ 384 / 512 bit 64 / 86 B 合规要求更强摘要
RS256 RSA PKCS#1 ≥ 2048 bit(建议 3072) 342 B 兼容性优先的分布式系统
PS256 RSA PSS ≥ 2048 bit 342 B 需要概率签名、抗 padding 攻击
ES256 ECDSA P-256 256 bit 86 B 移动端 / 带宽敏感(更短更快)
EdDSA Ed25519 256 bit 86 B 新系统首选,实现简单、无随机数陷阱

决策顺序

  1. 签发方 == 校验方,且只有一个服务 → HS256
  2. 有多个校验方或第三方 → RS256(兼容)或 ES256(更短)。
  3. 全新系统、两端都可控 → EdDSA,避开 ECDSA 的随机数复用风险。
  4. 涉及国密 / 合规要求 → 按规范选择 SM2

想生成密钥对,可以用 RSA 密钥生成器;想理解 HMAC 与哈希的差异,HMAC 生成器哈希工具 可以并排试用。

七步验证清单

验签不是「库说通过就算通过」。下面是生产环境必须走完的顺序:

  1. 结构检查:必须是三段,且每段都是合法 Base64Url。
  2. 算法白名单:从配置读取允许的 alg 列表,不接受令牌自带的 alg 决定用哪个密钥
  3. 定位密钥:按 kid 取;没有 kid 时按约定的单一密钥取。
  4. 验签:用上一步确定的算法和密钥验算,先于任何声明解析。
  5. 时效:校验 exp(含 leeway)、nbfiat 是否在未来。
  6. 来源:校验 issaud 是否匹配本服务。
  7. 业务:查 jti 是否在黑名单、用户状态是否正常、权限是否包含所需 scope。

代码里最容易漏的是第 2 步。绝大多数 JWT 漏洞不是密码学被攻破,而是服务端「信任了令牌自己声明的算法」。

// ✅ 正确:算法写死在验证侧
jwt.verify(token, publicKey, { algorithms: ['RS256'], issuer, audience });

// ❌ 危险:让库按 token 的 alg 自行选择
jwt.verify(token, key); // 攻击者可换成 HS256 并用公钥当 HMAC 密钥

七种经典攻击与防御

攻击 原理 防御
alg: none 去掉签名,服务端若不校验算法就放行 强制算法白名单,显式拒绝 none
算法混淆 RS256 服务被喂 HS256 令牌,攻击者用公开的公钥当 HMAC 密钥签名 验证侧固定算法 + 固定密钥类型
弱密钥爆破 HS256 用了 secret / 123456 这类短密钥,可离线枚举 密钥 ≥ 256 bit 随机,定期轮换
JWKS / jku 欺骗 令牌里塞入攻击者控制的 jku,服务端去拉假公钥 忽略 jku/x5u,公钥只从本地配置或固定 JWKS 端点取
kid 注入 kid 被当作文件路径或 SQL 片段拼进查询 kid 只做字典查表,绝不拼进路径或 SQL
XSS 窃取 令牌存 localStorage,被注入脚本读走 见 FAQ 存储建议 + 严格 CSP
重放 截获令牌后重复使用 短期 exp + jti 一次性校验 + HTTPS

顺带一句:令牌泄露后的爆炸半径取决于有效期。把 Access Token 从 7 天改成 15 分钟,是投入产出比最高的一次安全改进。

生命周期与撤销

双令牌模型

令牌 有效期 存放 作用
Access Token 5–15 分钟 内存 / Authorization 头 访问业务 API
Refresh Token 7–30 天 HttpOnly Cookie,绑定 /refresh 路径 换发新 Access Token

Refresh Token 必须轮换:每次换发都生成新的并作废旧的那枚。若检测到已作废的 Refresh Token 再次被使用,说明令牌已泄露——立即吊销整条令牌链并强制重新登录。

三种撤销策略对比

策略 实现 生效延迟 代价
缩短有效期 改配置 ≤ Access Token 寿命 刷新请求变多
黑名单(jti Redis 存未过期的已撤销 id 即时 重新引入状态;只存未过期的可自动过期
版本号(token_version 用户表存版本,Payload 带上,校验时比对 即时 每次校验一次查询(可缓存)

需要「立即下线某人」的合规场景用黑名单;普通产品用「短有效期 + 版本号」即可。

相关状态码

令牌相关错误建议这样返回,配合 HTTP 状态码速查

情况 状态码 响应头
令牌过期 401 WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
签名无效 401 同上,日志里记录 jti 便于追踪
权限不足 403 不要返回 404,二者语义不同
刷新令牌失效 401 清除 Cookie,强制重新登录

各语言推荐库

语言 推荐库 备注
Node.js jose(首选)、jsonwebtoken jsonwebtoken 必须显式传 algorithms
Python PyJWTpython-jose PyJWT 需自行校验 iss/aud
Java nimbus-jose-jwtjjwt Spring Security 内置 NimbusJwtDecoder
Go golang-jwt/jwt/v5lestrrat-go/jwx v5 起要求显式声明算法
PHP firebase/php-jwtlcobucci/jwt 后者 API 更现代
Rust jsonwebtoken 注意时钟 leeway 配置

什么时候不该用 JWT

  • 需要秒级吊销的管理后台——Session 更简单也更安全。
  • 把 JWT 当 Session 全量数据桶——Payload 每次请求都要传,塞太多会拖慢接口、撑爆请求头(部分网关限制 8 KB)。
  • 用它替代加密传敏感数据——那是 JWE 或 TLS 的活。
  • 单一单体应用的简单登录——引入的复杂度未必换来收益。

上线前自检

  • [ ] alg 由服务端白名单决定,不接受令牌自带值
  • [ ] 密钥 ≥ 256 bit 随机,且不进代码仓库
  • [ ] exp / nbf / iat 使用秒级时间戳
  • [ ] 校验了 issaud
  • [ ] leeway 设置在 30–60 秒
  • [ ] Payload 中无任何敏感数据
  • [ ] Access Token ≤ 15 分钟,Refresh Token 有轮换
  • [ ] 忽略 jku / x5u,公钥来源固定
  • [ ] 全站 HTTPS,令牌不出现在 URL 与日志里
  • [ ] 有 jti,具备撤销能力

现在可以打开 JWT 解析器,把任意令牌粘进去看三段结构——整个过程在你的浏览器里完成,令牌不会上传到任何服务器。

广告

常见问题

JWT 的 Payload 明明看起来是乱码,为什么说是明文?

JWT 用的是 Base64Url 编码,不是加密。任何工具都能在三秒内还原成 JSON。把 [JWT 解析器](/jwt-parser.html) 打开,或者手动把中间那段丢进 [Base64 转换器](/base64-converter.html),Payload 立刻可读。所以密码、身份证号、密钥这类数据一律不能放进去,只放非敏感标识与过期时间。

HS256 和 RS256 到底该选哪个?

只有一个服务签发也只有一个服务校验时,用 HS256 最简单:一把共享密钥,计算快,实现不容易出错。凡是「签发方和校验方不是同一个服务」——微服务、第三方 API、移动端后端——都用 RS256 或 ES256:私钥签发、公钥校验,校验方根本不需要能签名的能力,公钥泄露也不影响安全。ES256 签名更短更快,RS256 兼容性最好。

JWT 签发了就没法撤销吗?

传统 Session 可以在服务端直接删,JWT 默认不行——因为它自包含,服务端无状态。三种折中方案:① 缩短有效期(Access Token 5–15 分钟),把撤销代价降到可接受;② 维护黑名单,只存已撤销且尚未过期的 jti,代价是有状态;③ 在用户表存 token_version,签发时写进 Payload,改密码就自增,校验时比对,代价是一次查询。绝大多数业务用 ①+③ 就够了。

JWT 应该存在 localStorage 还是 Cookie?

两者都不是绝对安全,区别在于失守的方式。localStorage 会被任意 XSS 脚本直接读走,但你能精确控制发送时机;HttpOnly + Secure + SameSite 的 Cookie 读不走(XSS 拿不到),但会随每个同站请求自动带上,需要额外防 CSRF。当前主流建议是:Access Token 放内存(变量/闭包),Refresh Token 放 HttpOnly Cookie 并绑定到专用路径,Cross-site 场景用 SameSite=None 配合 CSRF token。

为什么我的 JWT 校验在容器里总是报「token not yet valid」?

nbf(Not Before)是服务端签发时间,客户端与服务器时钟不同步就会误判。Kubernetes 节点、CI 环境、跨时区部署尤其常见。两个动作:一是签发时用 NTP 同步过的单一时间源,二是校验库留 30–60 秒的 leeway(宽容窗口),jsonwebtoken 是 `clockTolerance`,PyJWT 是 `leeway`。注意 leeway 同样适用于 exp,别设成几分钟以上,否则等于延长了令牌寿命。

← 返回博客首页