2. JWT结构深度解析
一个JWT在网络上传输时,看起来就是三段由英文句点“.”分隔的字符串,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzAwMDAwMDAwfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这三段分别对应 Header(头部)、Payload(载荷) 和 Signature(签名)。理解JWT安全,第一步必须清楚:前两段只是“编码”,不是“加密”;第三段才是防篡改的关键。下面这张图展示了三段结构、编码方式与安全职责的对应关系。
Header:算法声明与类型标识
Header 是一个JSON对象,最常见字段是 alg 和 typ。typ 通常为 "JWT",表示令牌类型;alg 声明签名算法,如 HS256、RS256、ES256。Header 经 Base64URL 编码后成为第一段。
Header 的安全含义非常关键:alg 是攻击者最常瞄准的字段。经典攻击包括把 alg 改为 none,或把非对称算法 RS256 改为对称算法 HS256,诱使服务端用公钥当HMAC密钥验签。防御原则是:服务端必须固定允许的算法白名单,绝不信任令牌自带的 alg 字段。
Payload:声明集合与字段语义
Payload 也是JSON对象,承载实际业务声明。声明分三类:注册声明(标准定义)、公共声明(自定义但需防冲突)、私有声明(双方约定)。典型注册声明及其安全含义如下:
| 声明 | 全称 | 语义 | 安全含义 |
|---|---|---|---|
iss | Issuer | 签发者 | 必须校验,防止其他系统签发的令牌混入 |
sub | Subject | 主体(用户ID) | 标识身份,需与授权逻辑绑定 |
aud | Audience | 接收方 | 必须校验,防止令牌被用于非目标服务 |
exp | Expiration | 过期时间 | 必须校验,过期令牌一律拒绝 |
nbf | Not Before | 生效时间 | 早于该时间不可用,防提前使用 |
iat | Issued At | 签发时间 | 可用于判断令牌年龄、辅助风控 |
jti | JWT 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 的改动都会导致验签失败。
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,这正是签名段的价值所在。