签发 JWT 时,首先要决定的就是 HS256 还是 RS256。这两个 alg 值看起来相似,但 HS256 用共享密钥(HMAC-SHA256)对令牌进行认证,而 RS256 用 RSA 私钥(RSASSA-PKCS1-v1_5 配合 SHA-256)对令牌签名。这决定了谁能生成令牌、密钥如何分发,以及密钥泄露后会发生什么。
概览
| HS256 | RS256 | |
|---|---|---|
| 算法 | HMAC 配合 SHA-256(RFC 7518 §3.2) | RSASSA-PKCS1-v1_5 配合 SHA-256(§3.3) |
| 密钥 | 一个共享密钥,至少 256 位 | RSA 密钥对,至少 2048 位 |
| 谁能验证 | 共享密钥的持有者 | 任何拥有公钥的人 |
| 谁能签发令牌 | 每一个共享密钥的持有者,包括验证方 | 只有私钥持有者 |
| 密钥分发 | 向每个验证方保密地提供一份副本 | 公开的 JWKS(jwks_uri) |
| 签名部分 | 32 字节,43 个 base64url 字符 | 256 字节,342 个字符(RSA-2048) |
| 示例令牌(实测见下文) | 187 个字符 | 508 个字符 |
| 速度(Apple M4 Max 单核) | 每秒数百万次 HMAC | 每秒约 3,000 次签名、114,000 次验证 |
| JWA 要求级别 | 必需(Required) | 推荐(Recommended) |
| 主要失效方式 | 密钥过弱或泄露:任何人都能签发令牌 | 宽松的验证方中出现算法混淆;密钥处理不当 |
信任模型
使用 HS256 时,验证密钥和签名密钥是同一串字节。每个验证令牌的服务也都能为任意用户、范围或受众生成有效令牌;只要其中任何一个服务泄露了密钥(日志中的环境变量、前端打包产物、移动应用),任何人都能签发令牌。对于自己签发、自己验证会话令牌的单体应用,这可以接受;但如果一个令牌颁发者服务于许多 API 或团队,这就是严重的弱点。
使用 RS256 时,颁发者保管私钥(最好放在 HSM 或 KMS 中),并在其元数据所列的 URL 上以 JWKS 的形式发布公钥。验证方获取密钥集,按 kid 选择密钥;增加或移除验证方都不需要共享密钥。被攻陷的验证方能读取令牌,但无法伪造令牌。这就是 OpenID Connect 提供商默认用 RS256 对 ID 令牌签名的原因,也是 JWT 最佳实践 RFC(RFC 8725)引导多方部署采用非对称算法的原因。
OpenID Connect 确实允许机密客户端使用 HS256 ID 令牌,其密钥就是客户端密钥;这之所以可行,是因为每个客户端只验证签发给自己的令牌。
密钥强度与离线破解
HS256 令牌是完美的离线测试预言机:任何看到一个令牌的人,都能以自己的硬件计算 HMAC-SHA256 的速度尝试候选密钥,完全不需要与你的服务器通信。短的、人为选取的或默认的密钥(“secret”、项目名称、教程中的示例)用常见的密码破解工具几秒钟就能破解。RFC 7518 要求密钥长度至少等于哈希输出的长度,对 HS256 来说是 256 位;RFC 8725 §3.5 规定密钥必须具有足够的熵。用 CSPRNG 生成密钥(openssl rand -base64 32),切勿从密码派生密钥。
RS256 的私钥没有类似的离线攻击方式,但 RFC 7518 不允许使用小于 2048 位的 RSA 密钥,而且密钥文件必须像任何私钥一样妥善保护。
两者之间的算法混淆
最著名的 JWT 漏洞正是把两者混在了一起。某个服务调用库来验证 RS256 令牌,传入的是 PEM 格式的 RSA 公钥。如果该库从令牌头部读取算法,同时又支持 HS256,攻击者就可以发送一个 "alg": "HS256" 的令牌,其 HMAC 以公钥的 PEM 字节作为密钥计算;库用相同的字节进行验证,伪造的令牌便能通过验证。Tim McLean 于2015年连同 "alg": "none" 一起描述了这个问题,RFC 8725 §2.1 和 §3.1 把修复方法写进了规范:每个密钥可以配合哪种算法使用,由验证方决定,而不是由令牌决定。
现代库让这种攻击很难得逞:jsonwebtoken 拒绝把公钥用作 HMAC 密钥(配置把两者弄混时,会出现错误 secretOrPublicKey must be an asymmetric key),PyJWT 要求传入 algorithms 参数,jose 则把算法与密钥类型绑定。尽管如此,仍应始终传入明确的允许列表,其中只包含你预期的那一种算法。
大小与速度
下面的脚本用 Node.js 26.10 运行,只用 node:crypto 把同一组声明分别构造成 HS256 令牌和 RS256 令牌。
import { createHmac, generateKeyPairSync, randomBytes, sign } from "node:crypto";
const b64url = (value) => Buffer.from(value).toString("base64url");
const payload = b64url(JSON.stringify({ iss: "https://auth.example.com", sub: "user-42", aud: "api", exp: 1792500000 }));
// HS256: HMAC-SHA256 with a shared 256-bit secret.
const secret = randomBytes(32);
const hsInput = `${b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${payload}`;
const hs256 = `${hsInput}.${b64url(createHmac("sha256", secret).update(hsInput).digest())}`;
// RS256: RSASSA-PKCS1-v1_5 with SHA-256 and a 2048-bit key.
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const rsInput = `${b64url(JSON.stringify({ alg: "RS256", typ: "JWT", kid: "2026-10" }))}.${payload}`;
const rs256 = `${rsInput}.${b64url(sign("sha256", Buffer.from(rsInput), privateKey))}`;
console.log("HS256 token:", hs256.length, "characters, signature part:", hs256.split(".")[2].length);
console.log("RS256 token:", rs256.length, "characters, signature part:", rs256.split(".")[2].length);
HS256 token: 187 characters, signature part: 43
RS256 token: 508 characters, signature part: 342
RS256 的头部还带有 kid,验证方需要用它从 JWKS 中选出密钥。在生产环境中,应使用持续维护的 JWT 库,而不是手工构造令牌;这里关注的是签名部分的大小。关于 ECDSA 在同一取舍上的对比,参见 ES256 与 RS256 对比。
如何选择
- 单个应用签发并验证自己的会话令牌:HS256,使用 32 字节的随机密钥,轮换时保留较短的重叠期。
- 授权服务器的令牌由多个 API 验证:RS256、PS256 或 ES256,密钥发布在 JWKS 中。
- 令牌由第三方、移动应用或浏览器验证:只能用非对称算法;下发到客户端的密钥就等于公开了。
- 面向机密客户端的 OpenID Connect ID 令牌:两者都可以;非对称算法是默认选择,运维也更简单。
- 发给单个客户的 Webhook:HMAC 类方案很常见,因为每个客户都有自己的密钥。
- 合规驱动的密钥托管(HSM、KMS):非对称密钥,因为服务无需接触签名密钥材料就能验证。
迁移说明
从 HS256 迁移到 RS256 或 ES256,而无需让所有系统在同一天同时切换:
- 在 KMS 或 HSM 中创建密钥对,并在 JWKS 中发布公钥,附上新的
kid和alg。 - 让验证方同时支持两者:按
kid选择密钥,并把每个密钥绑定到一种算法(共享密钥绑定 HS256,新密钥绑定 RS256)。切勿让同一次验证调用对同一份密钥材料同时接受两种算法。 - 使用新的
kid开始签发非对称令牌;已有的 HS256 令牌在过期之前仍能通过验证。 - 等最长的 HS256 令牌有效期过去后,从每个验证方中删除共享密钥。真正的安全收益来自这一步。
- 只要旧密钥有任何泄露的可能,无论如何都要轮换它,因为在验证方停止接受 HS256 令牌之前,持有该密钥的攻击者仍能签发这类令牌。
参考资料
- RFC 7518:JSON Web 算法(§3.2 HMAC,§3.3 RSASSA-PKCS1-v1_5)
- RFC 8725:JWT 最佳当前实践(§2.1、§3.1、§3.5)
- RFC 7519:JSON Web Token
- RFC 7517:JSON Web Key 与 JWK 集
- OpenID Connect Core 1.0 §10.1:使用 MAC 算法签名
- Auth0:JSON Web Token 库中的严重漏洞(2015年)