JWT 解析:标头、载荷、签名、base64url,以及为何解码不等于验证

三个点:JSON Web Token 的结构

JSON Web Token 是由三个以点号分隔的 base64url 编码值组成的字符串:base64url(标头).base64url(载荷).base64url(签名)。RFC 7519 于 2015 年 5 月发布,定义了这一格式。无论 JWT 出现在 Authorization 标头、Cookie 还是 URL 查询参数中,都遵循这个结构。三个部分可以独立解码,而签名部分以密码学保证将它们紧密连接。

这种格式的设计目标是紧凑且 URL 安全。在 JWT 出现之前,API 认证通常依赖服务器端存储的不透明会话标识符,每次请求都需要数据库查询。JWT 将声明直接嵌入令牌中,使无状态服务器无需查询即可验证。这种便利性伴随着责任:与服务器可以立即撤销的会话令牌不同,已签名的 JWT 在其 exp 声明过期前始终有效,除非服务器维护黑名单。

base64url 编码:base64 的 URL 安全变体

标准 base64 使用 64 个字符对二进制数据进行编码:A–Z、a–z、0–9、+/,并以 = 填充。其中两个字符(+/)在 URL 和 HTTP 查询字符串中具有特殊意义,若 base64 字符串出现在 URL 中则需要百分比编码。base64url 定义于 RFC 4648 第 5 节,通过将 + 替换为 -/ 替换为 _ 并完全省略尾部的 = 填充来解决这个问题。这样的结果可以直接用于 URL、HTTP 标头和 Cookie 中,无需进一步编码。

膨胀率与标准 base64 相同:每 3 个输入字节变为 4 个 base64url 字符——约 33% 的扩展比例。由于编码是可逆的且不需要任何密钥,任何获得 JWT 的人都可以通过解码读取其标头和载荷——base64url 是编码,不是加密

标头:算法选择与令牌类型

标头是一个 JSON 对象,告知接收方如何处理令牌。最基本的标头包含两个字段:alg(签名算法)和 typ(标准令牌固定为 "JWT")。RFC 7518 定义了算法标识符。最常见的有:HS256——HMAC-SHA256,一种对称算法,使用同一个密钥进行签名和验证;RS256——带 SHA-256 的 RSA-PKCS1v1.5,非对称,用私钥签名,用对应的公钥验证;ES256——使用 P-256 和 SHA-256 的 ECDSA,也是非对称,但签名比 RSA 更短。

对称与非对称算法的选择有实际的架构影响。使用 HS256 时,所有需要验证令牌的服务都必须拥有共享密钥,泄露给一个服务等同于泄露给所有服务。使用 RS256 或 ES256 时,签发方保持私钥私密,只公开公钥(通常通过 JWKS 端点),验证服务不需要接触签名材料。对于多服务或第三方集成,非对称算法更为合适。

载荷:已注册声明及其含义

载荷是包含声明的 JSON 对象——关于实体的陈述,通常是已认证的用户,以及令牌本身的元数据。RFC 7519 第 4.1 节定义了七个已注册声明名称,其语义已标准化:iss(签发方)、sub(主题,令牌所涉及的主体,通常是用户 ID)、aud(受众,令牌的预期接收方)、exp(过期时间,超过此 NumericDate 后令牌不得被接受)、nbf(生效时间,在此 NumericDate 之前令牌不得被接受)、iat(签发时间)以及 jti(JWT ID,防止重放攻击的唯一标识符)。

NumericDate 是自 1970-01-01T00:00:00Z UTC(Unix 纪元)以来的秒数,不是毫秒。熟悉 JavaScript Date.now()(返回毫秒)的开发者在编写令牌验证代码时若未进行转换,这是常见的错误来源。标准 JWT 的载荷已签名但未加密——任何截获令牌的人都可以读取所有载荷声明。若载荷包含敏感数据,应使用 JWE(RFC 7516)进行加密,或仅通过 TLS 传输。

签名:它保护什么——以及 alg:none 的陷阱

签名计算基于签名输入,即 base64url(标头) + "." + base64url(载荷)。对于 HS256,签名是 HMAC-SHA256(密钥, 签名输入) 再进行 base64url 编码。由于签名输入包含标头和载荷两者,对任一部分的任何修改——即使只改变一个字符——都会使签名无效。正确验证签名的服务器可以确定令牌自签发以来未被篡改。

2015 年,多个 JWT 库中发现了一类广为人知的漏洞:alg:none 攻击。RFC 7519 在技术上允许 "alg":"none" 表示无签名的未受保护 JWT。缺陷在于一些库从(未验证的)标头读取 alg 字段来选择验证路径——如果攻击者将标头改为 "alg":"none" 并移除签名,库会在不检查任何签名的情况下声明令牌有效。CVE-2015-9235 记录了流行的 node-jsonwebtoken 库中的这个问题。解决方案是:验证方必须明确指定预期的算法,并拒绝标头中使用不同算法的任何令牌。

解码与验证:为何这个区别至关重要

解码 JWT 是指对三个部分进行 base64url 解码,以恢复 JSON 标头、载荷和原始签名字节。不需要任何密钥。任何持有 JWT 字符串的人都可以解码并读取声明。这是设计使然——JWT 并非不透明容器。调试认证问题的开发者、读取访问日志的运维工程师、审计系统的安全研究员:所有人都可以解码任何他们拥有的 JWT。这正是 JWT 解码工具有价值的原因。

验证 JWT 是指检查签名是否由正确的密钥对精确的标头和载荷字节产生。验证成功可以证明两件事:令牌由签名密钥持有者创建(真实性),以及自签名以来标头和载荷均未被更改(完整性)。验证需要密钥——HMAC 算法需要共享密钥,RSA/ECDSA 需要公钥。

关键错误是将解码的 JWT 视为可信而不进行验证。如果客户端从 localStorage 中读取 JWT 声明并直接使用,而不进行服务器端签名验证,就是在信任任何用户都可以自由伪造的数据。攻击者可以制作 {"sub":"admin","role":"superuser"},用任意标头进行 base64url 编码,并附上伪造或空签名——这样的令牌完全可以被解码。授权决策必须在服务器端进行密码学验证之后做出。 JWT 解码工具是检查和调试的正确工具,但永远不能替代生产应用中的服务器端验证。