JWT 详解:结构、安全性与常见错误
深入了解 JSON Web Token 的工作原理、内部结构,以及开发者在生产环境中常踩的安全陷阱。
JSON Web Token(JWT)无处不在——它为全球范围内的单页应用、移动应用和微服务架构提供身份验证支持。然而,它也是 Web 开发中最容易被误解的安全基础组件之一。一旦用错,可能导致身份验证绕过、权限提升,甚至账户被完全接管。
什么是 JWT?
JWT 是一种紧凑、URL 安全的字符串,用于编码一组声明(claims)——即关于用户或会话的断言。它看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
它由三个以点号分隔的 Base64URL 编码段组成:
- Header(头部) — 算法与令牌类型
- Payload(负载) — 声明内容(用户数据)
- Signature(签名) — 完整性的加密证明
将任意 JWT 粘贴到我们的 JWT Decoder 中,即可立即检查所有三个部分,无需请求任何外部服务器。
JWT 的结构解析
Header(头部)
{
"alg": "HS256",
"typ": "JWT"
}
alg 指定签名算法,常见值包括:
HS256— 基于 SHA-256 的 HMAC(对称加密,使用单一密钥)RS256— 基于 SHA-256 的 RSA(非对称加密,使用公私钥对)ES256— 基于 SHA-256 的 ECDSA(非对称加密,密钥更短)
Payload(负载)
{
"sub": "user_abc123",
"email": "alice@example.com",
"role": "admin",
"iat": 1711670400,
"exp": 1711756800
}
标准注册声明:
| 声明 | 含义 |
|---|---|
sub |
Subject(令牌所描述的主体) |
iss |
Issuer(令牌的签发方) |
aud |
Audience(令牌的接受方) |
exp |
Expiration time(过期时间,Unix 时间戳) |
iat |
Issued at(签发时间,Unix 时间戳) |
nbf |
Not before(在此时间之前不接受该令牌) |
Signature(签名)
以 HS256 为例:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
签名用于证明令牌未被篡改。Payload 并未加密——它只是经过编码。 任何人都可以读取其内容。
切勿在 JWT Payload 中存储密码或信用卡号等敏感数据。
HS256 与 RS256:如何选择?
HS256 使用单一共享密钥。所有需要验证令牌的服务都必须持有相同的密钥。对于单体应用来说简单易用,但在多服务架构中存在风险——一旦任何一个服务被攻破,攻击者便可伪造令牌。
RS256 使用非对称密钥。认证服务器用私钥签名,其他所有服务用公钥验证。即使某个消费服务被攻破,攻击者也无法伪造令牌。对于多服务系统,推荐使用 RS256。
alg: none 漏洞
这是最臭名昭著的 JWT 攻击之一。某些早期版本的库会接受 "alg": "none" 且不带签名的令牌,并将其视为有效。攻击者可以构造如下 Header:
{ "alg": "none" }
配合任意 Payload,从而完全绕过身份验证。
修复方案: 在 JWT 库中始终显式指定允许的算法,绝不接受 none。
// ❌ 危险写法
jwt.verify(token, secret);
// ✅ 安全写法——仅接受 HS256
jwt.verify(token, secret, { algorithms: ["HS256"] });
算法混淆攻击
另一个严重漏洞:如果你的库从 Header 中自动检测算法,攻击者可以将 RS256 改为 HS256,并使用公钥作为 HMAC 密钥对令牌进行签名(而公钥,顾名思义,是公开的)。
修复方案: 始终在代码中硬编码预期的算法,绝不信任 alg Header。
令牌存储:JWT 应该放在哪里?
| 存储方式 | XSS 风险 | CSRF 风险 | 说明 |
|---|---|---|---|
localStorage |
高 | 无 | 页面上的任意 JS 均可访问 |
sessionStorage |
高 | 无 | 标签页关闭后清除 |
| HTTP-only Cookie | 无 | 中 | Web 应用首选;配合 SameSite=Strict 使用 |
| 内存(变量) | 低 | 无 | 页面刷新后丢失;适合单页应用 |
对于 Web 应用,配合 SameSite=Strict 的 HTTP-only Cookie 是最安全的选择。对于原生应用,应使用系统提供的安全存储 API(如 Keychain、Keystore)。
过期时间与刷新令牌
短期访问令牌(5–15 分钟)与长期刷新令牌配合使用是标准模式:
- 用户登录 → 服务器签发访问令牌(15 分钟)+ 刷新令牌(7 天,存储于数据库)
- 客户端使用访问令牌发起 API 请求
- 访问令牌过期后,客户端发送刷新令牌 → 获取新的访问令牌
- 用户登出时,在数据库中使刷新令牌失效
这种模式能有效限制访问令牌被盗后的危害范围。
吊销 JWT
JWT 是无状态的——一旦签发,在过期之前始终有效。这是一种权衡。提前吊销的方案:
- 黑名单 — 将已失效的 JTI(JWT ID)存储在 Redis 中,每次请求时进行检查。
- 短期过期 — 5 分钟的令牌可将影响范围降至最低。
- 刷新令牌轮换 — 检测已轮换令牌的重复使用,将其视为被盗迹象。
使用 JWT Decoder 进行调试
在排查身份验证问题时,可使用我们的 JWT Decoder 来:
- 无需编写代码即可检查完整的 Payload
- 以可读格式查看过期时间戳
- 确认正在使用的签名算法
- 发现意外或缺失的声明
所有解码操作均在浏览器本地完成——令牌不会离开你的设备。
JWT 安全检查清单
- 在多服务架构中使用 RS256 或 ES256
- 在 JWT 库中显式指定允许的算法
- 每次请求都验证
exp、iss和aud声明 - Web 应用中将令牌存储于 HTTP-only Cookie
- 使用较短的访问令牌有效期(15 分钟或更短)
- 实现带重复使用检测的刷新令牌轮换机制
- 切勿在 Payload 中存放敏感数据
- 全程使用 HTTPS——明文传输的令牌等同于没有任何认证
JWT 功能强大且便于使用,但其安全性完全取决于实现方式。理解其结构、掌握已知攻击手段,你的身份验证层才能真正坚不可摧。