HandyTools Hub

← 全部指南

JWT 完全指南:三段结构、签名原理与常见误区

2026-08-06

打开任何一个需要登录的 Web 应用的请求头,你大概率能看到 Authorization: Bearer eyJhbGciOi... 这样一长串字符——这就是 JWT(JSON Web Token)。它是当今前后端分离架构里最常见的身份凭证格式:服务端签发一次,客户端每次请求带上它,服务端不用查数据库就能确认”你是谁”。但 JWT 也是误解重灾区:有人把它当加密容器塞敏感数据,有人只解码不验签就信任内容,结果捅出大篓子。这篇文章把 JWT 的结构、签名原理和经典坑讲清楚。

三段结构:Header.Payload.Signature

JWT 永远由三部分组成,用两个点 . 连接:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsImlhdCI6MTc1NDQ2NzIwMCwiZXhwIjoxNzU0NDcwODAwfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

第一段是 Header,声明令牌类型和签名算法。解码后就是一段 JSON:

{"alg": "HS256", "typ": "JWT"}

第二段是 Payload,承载实际的”声明”(claims),比如用户 ID、签发时间、过期时间:

{"sub": "12345", "iat": 1754467200, "exp": 1754470800}

第三段是 Signature,对前两段的签名。以 HS256 为例,它就是把 header + "." + payload 用密钥做 HMAC-SHA256,再编码一次。签名的作用是防篡改:任何人改了 Payload 里的任何一个字符,签名校验都会失败。

注意这三段都是 Base64URL 编码,不是普通 Base64。Base64URL 把标准 Base64 里的 +/ 换成 -_,并且去掉末尾的 = 填充——这样令牌才能安全地放在 URL 和请求头里。想亲手验证编码规则,可以把解码后的 JSON 再丢进 Base64 编解码器 对比结果。日常调试中,拿到一串 JWT 第一件事就是粘贴到 JWT 解码器 里,三个字段一目了然。

签名保证的是完整性,不是机密性

这是理解 JWT 最关键的一点:签名 ≠ 加密。Payload 只是 Base64URL 编码的 JSON,任何人拿到令牌都能解码出内容,根本不需要密钥。编码只改变”写法”,不改变”可读性”——把 eyJzdWIiOiIxMjM0NSJ9 解码成 {"sub":"12345"} 不需要任何权限。

签名真正保证的是完整性和来源可信

  • 令牌内容自签发后没有被改动过(篡改会导致签名校验失败);
  • 令牌确实由持有密钥的一方签发(前提是密钥没泄露)。

所以 JWT 的正确用法是:把”可以被全世界看到、但不能被伪造”的信息放进去——用户 ID、角色、权限范围、过期时间都是好公民;密码、手机号、身份证、密钥绝不能放。如果内容真的需要保密,要么别放 JWT 里,要么用 JWE(加密令牌),而不是寄希望于”别人看不懂这串乱码”。

HS256 与 RS256:对称与非对称的选择

JWT 头部 alg 字段指定签名算法,最常见的两个是 HS256 和 RS256。

HS256 是 HMAC-SHA256,对称算法:签名和验证用同一个共享密钥。签发方用密钥算出签名,验证方用同一个密钥重算一遍比对。它简单、快速,适合单一系统内部——密钥只有你自己的后端知道。缺点是任何能验证令牌的人也能伪造令牌,密钥分发的每一方都是潜在的泄露点。想直观感受 HMAC 的计算过程,可以用 哈希生成器 对一段文本做 HMAC-SHA256,换密钥结果就完全不同。

RS256 是 RSA 非对称算法:签发方用私钥签名,验证方用公钥验签。公钥可以公开分发(很多服务直接通过 /.well-known/jwks.json 暴露),任何人都能验证令牌真伪,但只有持有私钥的服务能签发新令牌。这在多服务架构里优势明显:网关、第三方服务只需公钥即可验签,私钥不出签发中心。

选择原则很简单:单体应用、密钥不出自家门,用 HS256;多方需要验签,或令牌跨系统流通,用 RS256

常见声明:iss、sub、exp、iat

JWT 规范(RFC 7519)定义了几个标准声明,都写在 Payload 里:

{
  "iss": "https://auth.example.com",
  "sub": "user-12345",
  "iat": 1754467200,
  "exp": 1754470800
}
  • iss(issuer):签发者,通常是认证服务的标识或 URL,验证方用它来确认”这令牌是不是我信任的服务签的”。
  • sub(subject):主体,通常是用户 ID,回答”这令牌代表谁”。
  • iat(issued at):签发时间,秒级 Unix 时间戳。
  • exp(expiration):过期时间,同样是秒级时间戳。当前时间超过 exp,令牌应立即被拒绝。上面这个例子里令牌有效期是一小时(7200 秒减 3600 秒等于 3600 秒)。

这些字段是保留名称,自定义数据(比如角色、昵称)请用别的键名,不要占用标准字段。另外还有个 nbf(not before)声明令牌在此时间前无效,以及 aud(audience)声明令牌的目标服务,在微服务间传递令牌时常用。

四个经典错误

1. 把机密塞进 Payload。 前面说过,Payload 人人可读。往里面放密码哈希、银行卡号、“内部备注”的事故屡见不鲜。记住:写进 Payload 之前先问自己,这段数据印在公告栏上可不可以?

2. 只解码,不验签。 解码 ≠ 验证。Base64URL 解码任何人都能做,不验签就信任 Payload 内容,等于信任一张没有任何防伪标志的纸条——攻击者完全可以自己伪造一个 sub: admin 的令牌。正确的验证流程是:校验签名 → 校验 alg 是否为预期算法 → 校验 exp/nbf/iss/aud → 然后才读 Payload。用 JWT 解码器 调试时也要分清:它帮你看内容,生产代码必须验签名

3. alg=none 的历史教训。 JWT 规范里允许 alg: "none",即不签名,用于”已有其他渠道保证安全”的场景。早年多个 JWT 库默认接受 none 算法,攻击者把 Header 改成 {"alg":"none"}、删掉签名段,就能伪造任意令牌。还有一类攻击是把 RS256 的令牌改成 HS256,用公钥当 HMAC 密钥去骗验证方。这些漏洞让现代库都强制要求显式指定允许的算法,不接受令牌自己声明的算法。这是一条铁律:验证时固定算法白名单。

4. 有效期过长且无吊销机制。 JWT 的无状态性是优点也是软肋:签发后服务端不需要存任何东西,反过来意味着想提前作废一个令牌很难。一个有效期 30 天的访问令牌泄露,攻击者就能用 30 天。业界通行做法是短期访问令牌(几分钟到几小时)+ 长期刷新令牌,刷新令牌存服务端、可吊销;高安全场景再加令牌黑名单或版本号机制。千万别为了”少点几次刷新接口”把 exp 设成一年。

调试过期与无效令牌

线上遇到”401 Invalid Token”,按这个顺序排查最快:

  1. 先解码看内容。 把令牌粘贴到 JWT 解码器,确认三段结构完整、Payload 是合法 JSON。结构对不上(比如只有两段、或者段间没有点),多半是被截断或复制不完整。
  2. exp 对照当前时间,秒级时间戳直接比对即可。过期是 401 最常见的原因——尤其是客户端时钟不准或服务端签了太短的令牌。
  3. 看 Header 的 alg 服务端期望 RS256,你拿了个 HS256 的令牌,验证必然失败。这在切换环境(测试/生产用不同签发配置)时高频出现。
  4. 怀疑编码问题。 令牌在 URL 里被二次编码、Base64 和 Base64URL 混用(- 被当成减号截断之类),都可能让签名段面目全非。可以用 Base64 编解码器 手动解码某一段验证内容是否完整。
  5. 验证失败但内容正常? 检查密钥/公钥是否匹配签发方,环境变量加载错误是常见根因。

需要说明的是,上面的”解码”都只是查看内容,不涉及密钥;真正的签名验证必须在你信任的后端用正确密钥完成。

小结

JWT 的成功在于它把一个信任问题变成了纯粹的密码学验证:拿到令牌,验签通过,就相信内容。记住四件事——三段结构都是 Base64URL 编码的明文;签名只保证完整性不保证机密性;HS256 共享密钥、RS256 公私钥分离;验证必须固定算法并检查 exp。理解了这四点,你在 JWT 上踩的坑会少掉九成。