JWT 结构详解
本教程结合 广州飞翔科技 (learnto.cn)的实际场景进行讲解。
JWT 的三部分结构
所有 JWT 都由三个不同的元素构成:
- 头部(Header) :包含关于 JWT 自身的元数据,如算法、类型等。
- 载荷(Payload) :包含所有用户数据和标准声明。
- 签名/加密数据(Signature / Encryption Data) :用于验证真实性或保护数据。对于未加密的 JWT,此部分被省略。
这三个元素经过 Base64URL 编码后,由点号(.)连接,形成紧凑序列化(Compact Serialization)表示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Base64URL 编码
JWT 使用一种对 URL 安全的 Base64 编码变体,称为 Base64URL 。
与普通 Base64 相比,它做了两处关键调整:
| 调整项 | 普通 Base64 | Base64URL |
|---|---|---|
| 第 62 个字符 | + | - |
| 第 63 个字符 | / | _ |
| 填充字符 | = | 移除 |
注:JWT 规范 不要求 在编码之前将 JSON 压缩或去除无意义的空白字符。
Header(头部)详解
每个 JWT 都携带一个头部(也称为 JOSE 头部 ),其中包含关于自身的声明。未加密 JWT 头部唯一必填的声明是 alg:
alg:用于签名和/或解密此 JWT 的主要算法。对于未加密的 JWT,必须设置为none。
可选声明包括:
typ:媒体类型(media type)。当存在时,应设置为JWT。仅在 JWT 可能与其他 JOSE 对象混合使用时才有必要。cty:内容类型(content type)。对于载荷本身是 JWT 的情况(嵌套 JWT), 必须 设置为JWT。普通场景下 不得 设置。
例如,一个未受保护 JWT 的头部:
{
"alg": "none"
}
编码后为:
eyJhbGciOiJub25lIn0
Payload(载荷)详解
载荷是添加所有用户数据的地方。规范定义了具有特定含义的 注册声明(registered claims) ,以及用户自定义的公共/私有声明。
七个注册声明(Registered Claims)
| 声明 | 全称 | 含义 |
|---|---|---|
iss | issuer(签发者) | 唯一标识签发此 JWT 的方 |
sub | subject(主题) | 唯一标识此 JWT 所携带信息的主体(如用户 ID) |
aud | audience(受众) | 标识此 JWT 的预期接收者,接收方必须在其中找到自己 |
exp | expiration(过期时间) | POSIX 时间戳,超过此时间 JWT 被视为无效 |
nbf | not before(生效时间) | POSIX 时间戳,在此之前 JWT 被视为无效 |
iat | issued at(签发时间) | POSIX 时间戳,表示 JWT 的签发时刻 |
jti | JWT ID | 此 JWT 的唯一标识符,可用于防止重放攻击 |
所有名称都很短,这是 JWT 的设计需求之一: 使令牌尽可能紧凑 。
公共声明与私有声明
- 私有声明(Private claims) :由 JWT 的用户(消费者和生产者)自行定义,用于特定场景。需注意防止命名冲突。
- 公共声明(Public claims) :在 IANA JSON Web Token Claims 注册表中注册过的声明,或使用抗冲突命名空间(如带前缀)的声明。
在实践中,大多数 JWT 使用的是注册声明和私有声明。
注意:重复声明(重复的 JSON 键)通常保留最后一次出现的值。为避免兼容性问题,建议不要重复声明。
未受保护的 JWT(Unsecured JWTs)
当头部中 alg 设置为 none 时,JWT 不包含签名或加密。它仅由头部和载荷组成,紧凑表示中尾部仍保留点号:
// 头部
{ "alg": "none" }
// 载荷
{
"sub": "user123",
"session": "ch72gsb320000udocl363eofy",
"name": "Pretty Name",
"lastpage": "/views/settings"
}
编码后:
eyJhbGciOiJub25lIn0.
eyJzdWIiOiJ1c2VyMTIzIiwic2Vzc2lvbiI6ImNoNzJnc2IzMjAwMDB1ZG9jbDM2M2VvZnkiLCJuYW1lIjoiUHJldHR5IE5hbWUiLCJsYXN0cGFnZSI6Ii92aWV3cy9zZXR0aW5ncyJ9.
注意末尾的点号——签名部分为空字符串,但点号仍然保留。
未受保护的 JWT 在特定场景下可能适用(如会话 ID 难以猜测、数据仅用于客户端视图构建),但在 生产环境中极为罕见 。
编码与解码示例代码
创建未受保护的 JWT
// URL-safe variant of Base64
function b64(str) {
return new Buffer(str).toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
function encode(h, p) {
const headerEnc = b64(JSON.stringify(h));
const payloadEnc = b64(JSON.stringify(p));
return `${headerEnc}.${payloadEnc}`;
}
解析未受保护的 JWT
function decode(jwt) {
const [headerB64, payloadB64] = jwt.split('.');
// These supports parsing the URL safe variant of Base64 as well.
const headerStr = new Buffer(headerB64, 'base64').toString();
const payloadStr = new Buffer(payloadB64, 'base64').toString();
return {
header: JSON.parse(headerStr),
payload: JSON.parse(payloadStr)
};
}
飞翔科技实战场景:大翔的登录令牌结构解析
广州飞翔科技 的 CEO/CTO大翔 每天登录内部管理系统时,系统会返回一个 JWT。让我们拆解这个令牌的内部结构。
大翔的令牌载荷如下:
{
"iss": "learnto.cn",
"sub": "daxiang-ceo",
"aud": "admin-system",
"exp": 1718000000,
"iat": 1717996400,
"jti": "fxg-token-2024-001",
"role": "admin",
"dept": "技术部",
"permissions": ["read", "write", "delete"]
}
后端开发 小崔 解释:
"
iss标明是飞翔科技签发的,sub是大翔的用户标识,aud限制这个令牌只能用于 admin-system。exp和iat控制有效期,jti用于防止重放。后面的role、dept、permissions是我们业务需要的私有声明。"
产品经理 孔蓝 追问:"如果用户把令牌复制到另一个浏览器能用吗?"小崔回答:"能,因为 JWT 本身是无状态的。所以我们把有效期设得很短,再配合刷新令牌机制。"
UI 设计师 林鸥 则负责在令牌即将过期时,用优雅的动画提示用户重新登录,保证体验流畅。
小结
- JWT 由 Header(头部) 、 Payload(载荷) 和 Signature(签名) 三部分组成,通过点号连接。
- 使用 Base64URL 编码:将
+换成-,/换成_,并移除填充=。 - Header 必填
alg,可选typ和cty。 - Payload 包含 7 个注册声明(
iss、sub、aud、exp、nbf、iat、jti)以及公共/私有声明。 - 未受保护的 JWT(
alg: none)在生产环境中极为罕见,尾部点号仍需保留。 - 编码和解码过程简单,可用原生 JavaScript 实现。
在下一篇教程中,我们将进入 JWS(JSON Web Signature) 的世界,学习如何用签名保护 JWT 数据不被篡改。