jwt decode security authentication tokens

深入理解 JWT:如何解码和生成 JSON Web Token

JSON Web Token (JWT) 全面指南。了解如何解码、验证和生成令牌,以便进行安全的身份验证调试。

简介:什么是 JSON Web Token?

JSON Web Token(JWT)是一项开放标准(RFC 7519),定义了一种紧凑、自包含的方式,用于以 JSON 对象的形式在各方之间安全传输信息。由于 JWT 经过数字签名,其内容可以被验证和信任。JWT 可使用密钥(HMAC 算法)或公私钥对(RSA 或 ECDSA)进行签名。

JWT 的诞生是为了解决分布式系统中的一个常见挑战:服务器如何在不查询会话数据库的情况下,信任来自客户端的请求?解决方案是将用户身份和权限直接编码并签名到一个令牌中,由客户端在每次请求时携带。

如今,JWT 已成为现代身份认证和授权的核心技术:

  • 单点登录(SSO): 一次登录,访问多个服务。
  • API 身份认证: 移动应用和单页应用(SPA)通过 Bearer Token 进行 API 调用认证。
  • OAuth 2.0 / OpenID Connect: JWT 被用作访问令牌(Access Token)和身份令牌(ID Token)。
  • 微服务架构: 各服务无需中央会话存储即可独立验证调用方身份。

JWT 结构:header.payload.signature

JWT 由三个 Base64url 编码的部分组成,以点(.)分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

第一部分 — Header(头部) 解码后为:

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

头部指定了签名算法(alg)和令牌类型(typ)。

第二部分 — Payload(载荷) 解码后为:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

载荷包含声明(Claims)——关于实体(通常是用户)的陈述和附加元数据。

第三部分 — Signature(签名)(以 HS256 为例)计算方式为:

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

签名确保令牌未被篡改。重要提示: 载荷只是 Base64url 编码,而非加密。任何持有 JWT 的人都可以解码并读取所有声明。

签名算法:HS256、RS256 与 ES256

HS256(HMAC-SHA256)— 对称算法

使用同一个共享密钥进行签名和验证。实现简单,但签发方和验证方都必须知道该密钥。

  • 适用场景: 单体应用或共享密钥的紧耦合服务。
  • 风险: 密钥一旦泄露,所有令牌均可被伪造。

RS256(RSA-SHA256)— 非对称算法

使用私钥签名,公钥验证。私钥保密;公钥可通过 JWKS 端点公开共享。

  • 适用场景: 分布式系统、微服务以及多服务验证场景。
  • 密钥长度: 建议使用至少 2048 位的 RSA 密钥。

ES256(ECDSA-SHA256)— 非对称算法

使用 P-256 曲线的椭圆曲线数字签名算法(ECDSA)。在同等安全强度下,签名比 RS256 更小。

  • 适用场景: 对性能敏感或带宽受限的环境。
算法 类型 密钥 签名大小 适用场景
HS256 对称 共享密钥 32 字节 单服务应用
RS256 非对称 RSA 密钥对 256+ 字节 分布式系统
ES256 非对称 EC 密钥对 64 字节 高性能 API

JWT 声明:已注册、公开与私有

声明是编码在载荷中的关于实体(通常是用户)的陈述和元数据。

已注册声明(IANA 标准定义)

声明 全称 描述
iss 签发方 签发令牌的主体(如 "auth.example.com"
sub 主题 令牌所指向的主体(如用户 ID)
aud 受众 令牌的目标接收方(服务标识符)
exp 过期时间 令牌失效的 Unix 时间戳
nbf 生效时间 令牌在此 Unix 时间戳之前不得使用
iat 签发时间 令牌被签发的 Unix 时间戳
jti JWT 唯一 ID 唯一标识符,用于防止重放攻击

公开声明

在 IANA JWT Claims Registry 中注册定义。示例:emailnamepictureroles

私有声明

由签发方和消费方自行约定的自定义声明。建议使用命名空间避免冲突:

{
  "https://example.com/tenant_id": "acme-corp",
  "https://example.com/roles": ["admin", "editor"]
}

JWT 身份认证流程

  1. 用户登录: 客户端将凭据(用户名 + 密码)发送至认证服务器。
  2. 令牌签发: 服务器验证凭据后,创建包含相应声明的 JWT,用密钥/私钥签名后返回客户端。
  3. 令牌存储: 客户端将 JWT 存储在内存、localStorage 或 HttpOnly Cookie 中。
  4. 携带令牌请求: 客户端在每次访问受保护资源时,在 Authorization 头中携带 JWT:
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    
  5. 令牌验证: 服务器提取令牌,验证签名,检查 exp,并根据声明授权或拒绝访问。
  6. 令牌刷新: 访问令牌过期后,客户端使用长效刷新令牌获取新的访问令牌,无需重新登录。
客户端            认证服务器           资源服务器
  |                    |                     |
  |-- POST /login ---->|                     |
  |<-- 200 + JWT ------|                     |
  |                    |                     |
  |-- GET /api (Authorization: Bearer JWT) ->|
  |                    |      验证 JWT        |
  |<----------- 200 + 数据 ----------------->|

安全注意事项

载荷并未加密

Base64url 编码不是加密! 任何持有 JWT 的人都可以立即解码并读取所有声明。绝对不要在 JWT 载荷中存储:

  • 密码或密码哈希
  • 银行卡号或其他财务数据
  • 非必要的敏感个人信息(PII)
  • 私钥或内部密钥信息

务必验证签名

// Node.js — jsonwebtoken 库
const jwt = require("jsonwebtoken");

try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded.sub); // 可信的用户 ID
} catch (err) {
  res.status(401).json({ error: "无效或已过期的令牌" });
}

设置较短的过期时间

访问令牌的有效期应较短(15 分钟至 1 小时)。使用刷新令牌维持长效会话,以降低令牌被盗的影响。

始终使用 HTTPS

永远不要通过未加密的连接传输令牌。被拦截的令牌等同于被盗的密码。

常见安全漏洞

1. 算法混淆攻击(CVE-2015-9235)

攻击者将头部的 alg 字段从 RS256 改为 HS256,然后用服务器的公钥作为 HMAC 密钥对令牌签名。如果服务器实现不当,会将公钥当作 HMAC 密钥接受该伪造令牌。

防御措施: 在服务端明确指定期望的算法:

jwt.verify(token, publicKey, { algorithms: ["RS256"] });

2. "none" 算法攻击

如果服务器接受 alg: "none",攻击者可以伪造不带签名的令牌。

防御措施: 明确拒绝 alg: "none" 的令牌。现代主流库已修复此问题,但旧版本仍可能存在漏洞。

3. 弱密钥(HS256)

使用短小或可猜测的 HMAC 密钥,攻击者可以通过 hashcat 等工具对捕获的令牌进行离线暴力破解。

防御措施: 使用至少 256 位的加密随机密钥:

openssl rand -hex 32

4. 缺少声明验证

未验证 issaudnbf 可能导致跨服务令牌复用或令牌提前使用。

5. 不安全的令牌存储

localStorage 可被页面上的任意 JavaScript 访问,使令牌易受 XSS 攻击。建议使用 HttpOnly Cookie 存储令牌。

JWT vs 会话令牌:无状态 vs 有状态

特性 JWT(无状态) 会话令牌(有状态)
存储位置 客户端 服务端(数据库或缓存)
可扩展性 极佳 — 无需服务端查询 需要共享会话存储
撤销能力 困难 — 有效期内无法失效 简单 — 从存储中删除即可
令牌大小 每次请求携带较大数据 令牌小,数据在服务端
微服务支持 理想 — 各服务独立验证 需要共享会话基础设施
载荷可见性 持有令牌的人均可读取 数据仅存储在服务端

选择 JWT 的场景: 构建分布式系统、无状态 REST API 或移动应用。 选择会话的场景: 需要即时撤销能力(如"所有设备退出登录")时。

令牌撤销的挑战

由于 JWT 是自包含的,在令牌过期前无法轻易撤销,常见的解决方案有:

  • 短期访问令牌 + 刷新令牌: 访问令牌快速过期;在数据库中撤销刷新令牌。
  • 令牌黑名单: 在 Redis 等快速存储中维护已撤销的 jti 集合(引入部分有状态性)。
  • 密钥轮换: 轮换签名密钥使所有令牌失效(会影响所有在线用户)。

OAuth 2.0 与 OpenID Connect

OAuth 2.0

OAuth 2.0 是一个授权框架,不强制要求特定的令牌格式。JWT 因其自包含、携带授权 scope 等特性,被广泛用作 OAuth 访问令牌:

{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "https://api.example.com",
  "scope": "read:profile write:posts",
  "exp": 1893456000
}

OpenID Connect(OIDC)

OpenID Connect 是构建在 OAuth 2.0 之上的身份层,引入了 ID Token 概念。ID Token 始终是一个 JWT,包含经过认证的用户的身份声明:

{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",
  "aud": "my-client-id.apps.googleusercontent.com",
  "email": "[email protected]",
  "name": "Jane Doe",
  "exp": 1893456000,
  "iat": 1893452400
}

OIDC 还定义了 JWKS 端点/.well-known/jwks.json),授权服务器在此发布其公钥。任何服务都可以获取这些公钥来验证令牌,无需预先交换密钥。

最佳实践

  1. 公开系统使用非对称算法(RS256/ES256) — 私钥安全保存,公钥可供任何人验证。
  2. 为访问令牌设置较短的 exp — 15 分钟是常见的安全默认值。
  3. 始终轮换刷新令牌 — 每次使用后签发新令牌,并使旧令牌失效。
  4. 验证所有相关声明 — 至少检查 issaudexpnbf
  5. 明确指定期望的算法 — 永远不要仅依赖令牌头部的 alg 字段。
  6. 载荷中不存储敏感数据 — 载荷是编码,不是加密。
  7. 始终使用 HTTPS — 永远不要通过未加密连接传输令牌。
  8. 安全存储令牌 — 优先使用 HttpOnlySecureSameSite=Strict Cookie,而非 localStorage。
  9. 实施令牌撤销策略 — 使用黑名单或短效令牌配合刷新令牌轮换机制。
  10. 保持 JWT 库更新 — 新版本会修复已知安全漏洞。

常见问题解答

Q:可以解密 JWT 载荷吗? A:JWT 载荷是 Base64url 编码,不是加密——无需任何密钥即可读取。如果需要保密性,请使用 JWE(JSON Web Encryption,RFC 7516),它会对载荷进行加密。

Q:decodeverify 有什么区别? A:decode 只是 Base64url 解码各部分并返回 JSON,不做任何安全检查;verify 还会根据您的密钥验证签名,并检查 exp 等时间声明。生产环境中务必使用 verify

Q:如何安全地处理令牌刷新? A:签发短效访问令牌(15–60 分钟)和长效刷新令牌(数天至数周)。在服务端存储刷新令牌以便撤销。访问令牌过期后,客户端向专用端点发送刷新令牌以获取新的访问令牌。

Q:JWT 既可以用于认证也可以用于授权吗? A:可以。在同一令牌中包含身份声明(用户是谁)和授权声明(用户能做什么,如 rolesscope)。注意控制令牌大小。

Q:如果 JWT 密钥泄露了怎么办? A:攻击者可以为任意用户伪造合法令牌。应立即轮换密钥(这会使所有现有令牌失效),调查泄露原因,并强制所有用户重新登录。

Q:应该选择 HS256 还是 RS256? A:当多个服务需要验证令牌,或验证方不完全可信时,使用 RS256(或 ES256)。仅当同一服务同时负责签发和验证令牌,且密钥可以严格管控时,才使用 HS256。

Q:JWT 最大可以有多大? A:建议将 JWT 控制在 4 KB 以内,以避免 HTTP 头部大小限制和性能问题。不要在载荷中嵌入大型数据结构,改为通过 ID 引用。