JWT 速查手册:结构、声明与几个安全误区

JWT 是「可验签的 JSON」而不是加密数据:三段结构怎么读、常用注册声明有哪些、HS256 与 RS256 怎么选、alg=none 与 localStorage 两个高频误区——一篇手册全部对齐。

约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

三段结构:header.payload.signature

三段各是 base64url 编码的 JSON,用点号连接。前两段任何人都能解开看(不是加密!),第三段是签名——它保证前两段「一个字节都没被改过」。手动看一眼的方法:前两段各做一次 base64url 解码即可。

解开一个典型 JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9      ← header  {"alg":"HS256","typ":"JWT"}
.eyJzdWIiOiIxMDAxIiwibmFtZSI6IkFsaWNlIiwiZXhwIjoxNzI3NTY4MDAwfQ
                                            ← payload {"sub":"1001","name":"Alice","exp":1727568000}
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← 签名(不可解,只能验)

常用注册声明(RFC 7519)

最容易踩的坑:exp 只是 payload 里的一个数字,库不会「自动」拒收过期 token——除非你在验证选项里显式开启过期检查。线上事故里「token 永不过期」多半是验证时没配 algorithms 白名单,或 ignoreExpiration 被误设为 true。

声明含义谁负责校验
iss签发者标识接收方比对是否信任的签发方
sub主体(通常是用户 ID)业务层取用
aud目标受众接收方确认 token 是发给自己的
exp过期时间(Unix 秒)库自动校验——但仅在你启用时
nbf / iat生效时间 / 签发时间接收方按需校验
jtitoken 唯一编号用于吊销名单(黑名单记 jti)

签名不是加密:payload 是明文

任何人拿到 JWT 都能解出完整 payload——手机号、邮箱、角色塞进去等于公开发布。JWT 的价值是「防篡改」:接收方用密钥重算签名比对,改过一个字符就验不过。需要保密的字段不放进 payload,或改用 JWE(加密版 JWT)。

HS256 还是 RS256:一把钥匙还是两把

  • HS256(对称):签发与验证用同一把密钥。快、实现简单;但每个需要「验证」的服务都得持有密钥——持有者越多,泄露面越大,且任何持钥者都能签发新 token。
  • RS256(非对称):私钥签发、公钥验证。公钥可以随便发——资源服务器只验不签,泄露公钥无风险。微服务与第三方接入的标准选择。
  • 经验法则:单体或内部两三个服务,HS256 够用;验证方一多、或有外部合作方,直接 RS256(配合 JWKS 端点轮换公钥)。

四个高频安全误区

  1. 「alg 由 token 自己声明,信它就行」→ 不行。攻击者把 alg 改成 none 再去掉签名,天真的库就不验签了。验证时必须在代码里写死允许的算法白名单,绝不信 token 自述。
  2. 「JWT 放 localStorage 和放 Cookie 一样」→ 不一样。localStorage 里的 token 被 XSS 读走即失守,且无法服务端作废(除非上吊销名单);HttpOnly Cookie 脚本读不到,天然抗 XSS,但要配 CSRF 防护。移动端 / 前后端分离存 localStorage 时,务必缩短 exp 并支持刷新。
  3. 「签了名就不会过期、不会被吊销」→ 签名只保证内容没改,不保证仍有效。过期靠 exp 校验,吊销要另建机制(短 exp + refresh token,或 jti 黑名单)。
  4. 「把权限全写进 payload,网关就不用管了」→ payload 在签发后就冻结。角色变更、封号都无法影响已发出的 token——高敏操作服务端必须再查一次当前权限。

过期与刷新的基本姿势

组合拳:access token 短命(5~15 分钟)+ refresh token 长命(可服务端吊销)。access 泄露的损失被短 exp 封顶;refresh 泄露可以立刻在服务端作废。这是「无状态」与「可吊销」之间目前的最佳折中。

验证端(Node jsonwebtoken 为例)
jwt.verify(token, PUBLIC_KEY, {
  algorithms: ["RS256"],   // 白名单写死,不信 token 自述
  issuer: "auth.example.com",
  audience: "api.example.com",
  clockTolerance: 30       // 容忍 30 秒时钟偏差
});

常见问题

JWT 和 Session 到底怎么选?

Session 把状态放服务端(内存 / Redis),天然可吊销、实现简单,单体应用的默认选择;JWT 把状态放在 token 里,验证方无需查库,适合多服务、跨域与移动端。判断标准一句话:需不需要「立刻踢人下线」?需要 → Session 或 JWT+吊销名单;不需要且验证方多 → JWT。先用 JWT 解析工具看看现有 token 里到底放了什么,是优化的第一步。

为什么说 alg=none 是真实存在过的漏洞?

早年不少 JWT 库把「token 声明的算法」当真:攻击者把 header 改成 {"alg":"none"} 并删掉签名段,库看到 none 就跳过验签,伪造的 payload 直接生效——多个知名库都中过招。防御只有一条:验证时算法白名单由代码写死,与 token 自述无关。

JWT 该放 localStorage 还是 Cookie?

各有代价:localStorage 方便但 XSS 可读且难吊销;HttpOnly + Secure + SameSite 的 Cookie 抗 XSS 但要防 CSRF,且跨域配置更繁琐。前后端分离 + 移动端通常 localStorage(短 exp + refresh 兜底);传统 Web 应用推荐 HttpOnly Cookie。关键是想清楚你的威胁模型里 XSS 和 CSRF 哪个更近。

JWT 签名验证失败,最常见的几个原因?

按出现频率排:① 密钥不匹配——签发用 HS256、验证却按 RS256 的公钥试,或 secret 复制带了首尾空格;② 算法白名单没包含 token 实际的 alg(库名义上支持,verify 选项里没列);③ iss / aud 断言不符——多环境共用密钥时高发;④ 时钟偏差——两台机器差几十秒,exp / nbf 刚好卡在边界,加 clockTolerance 即可。把 token 粘进 JWT 解析工具核对 header 与声明,再用签名工具重算比对,两步就能定位是哪一类。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件