2. JWT结构深度解析

一个JWT在网络上传输时,看起来就是三段由英文句点“.”分隔的字符串,例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzAwMDAwMDAwfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这三段分别对应 Header(头部)、Payload(载荷) 和 Signature(签名)。理解JWT安全,第一步必须清楚:前两段只是“编码”,不是“加密”;第三段才是防篡改的关键。下面这张图展示了三段结构、编码方式与安全职责的对应关系。

JWT 三段结构与安全职责 Header 头部 {"alg":"HS256", "typ":"JWT"} Base64URL 编码 决定签名算法,可被篡改 Payload 载荷 {"sub":"123", "exp":1700003600} Base64URL 编码 明文可读,禁止放敏感数据 Signature 签名 HMACSHA256( base64url(h)+"."+base64url(p), secret) 防篡改,验证完整性 Base64URL 编码要点 • 标准 Base64 的 “+” 替换为 “-”,“/” 替换为 “_” • 去掉末尾填充符 “=”,保证 URL 与文件名安全 • 编码 ≠ 加密:任何人可解码查看 Header 与 Payload 内容 • 签名只覆盖前两段,任何字段改动都会导致验签失败

Header:算法声明与类型标识

Header 是一个JSON对象,最常见字段是 alg 和 typ。typ 通常为 "JWT",表示令牌类型;alg 声明签名算法,如 HS256、RS256、ES256。Header 经 Base64URL 编码后成为第一段。

Header 的安全含义非常关键:alg 是攻击者最常瞄准的字段。经典攻击包括把 alg 改为 none,或把非对称算法 RS256 改为对称算法 HS256,诱使服务端用公钥当HMAC密钥验签。防御原则是:服务端必须固定允许的算法白名单,绝不信任令牌自带的 alg 字段。

Payload:声明集合与字段语义

Payload 也是JSON对象,承载实际业务声明。声明分三类:注册声明(标准定义)、公共声明(自定义但需防冲突)、私有声明(双方约定)。典型注册声明及其安全含义如下:

声明全称语义安全含义
issIssuer签发者必须校验,防止其他系统签发的令牌混入
subSubject主体(用户ID)标识身份,需与授权逻辑绑定
audAudience接收方必须校验,防止令牌被用于非目标服务
expExpiration过期时间必须校验,过期令牌一律拒绝
nbfNot Before生效时间早于该时间不可用,防提前使用
iatIssued At签发时间可用于判断令牌年龄、辅助风控
jtiJWT ID唯一标识支持防重放、黑名单与撤销

时间类声明 exp、nbf、iat 均为 NumericDate,即自1970-01-01T00:00:00Z起的秒数。校验时必须考虑时钟偏移,通常允许几十秒的容差(如 clockTolerance: 30)。jti 是防重放的核心:服务端可维护已使用 jti 集合,同一 jti 二次出现即拒绝。

Signature:完整性的唯一保障

签名段的计算方式为:HMACSHA256(base64url(header) + "." + base64url(payload), secret),再对结果做 Base64URL 编码。对于 RS256/ES256,则用私钥签名、公钥验签。签名不覆盖任何额外内容,只覆盖前两段的编码字符串,因此任何 Header 或 Payload 的改动都会导致验签失败。

常见误区:把 Payload 当作加密数据,在其中放入密码、身份证号、内部密钥等敏感信息。Payload 只是 Base64URL 编码,任何人拿到令牌即可解码查看。敏感数据必须放在服务端,或使用 JWE 加密令牌。

Base64URL 编码细节

Base64URL 是 Base64 的URL安全变体,规则有三点:

  • 字符 + 替换为 -,字符 / 替换为 _;
  • 去掉末尾的填充符 =;
  • 解码时需按长度补回 =,再按标准 Base64 还原。

例如字符串 {"alg":"HS256"} 经 Base64URL 编码后为 eyJhbGciOiJIUzI1NiJ9。注意它没有 =,且不含 + 与 /,因此可以安全地放在 URL、Cookie 和 HTTP 头中传输。

实操:解码与验签观察

以 Node.js 的 jsonwebtoken 为例,签发与验证过程如下:

const jwt = require('jsonwebtoken');
const secret = 'demo-secret-do-not-use-in-prod';

// 签发
const token = jwt.sign(
  { sub: 'user-123', aud: 'api.example.com', jti: 'a1b2c3' },
  secret,
  { algorithm: 'HS256', issuer: 'auth.example.com', expiresIn: '15m' }
);

// 验证:固定算法、校验 iss/aud、允许30秒时钟偏移
try {
  const payload = jwt.verify(token, secret, {
    algorithms: ['HS256'],
    issuer: 'auth.example.com',
    audience: 'api.example.com',
    clockTolerance: 30
  });
  console.log('验签通过', payload.sub, payload.jti);
} catch (e) {
  console.error('验签失败', e.message);
}

观察要点:把 algorithms 固定为 ['HS256'] 可阻断 alg 混淆攻击;校验 issuer 与 audience 可阻断跨系统令牌混用;clockTolerance 避免因服务器时间偏差导致误拒。若手动修改 Payload 中任意字符,验签会立即抛出 invalid signature,这正是签名段的价值所在。

快速判断口诀:Header 看算法是否被信任,Payload 看声明是否被校验,Signature 看密钥是否被保护。三者任一失守,JWT 安全即被突破。