跳到主要内容
ecdsa.com

HS256 vs RS256:JWT 用共享密钥还是公钥

HS256 是 HMAC:每个验证方也都能签发令牌。RS256 是签名:验证方只持有公钥。大多数 JWT 设计错误,都始于没有注意到这一区别。
对比对象HS256RS256

签发 JWT 时,首先要决定的就是 HS256 还是 RS256。这两个 alg 值看起来相似,但 HS256 用共享密钥(HMAC-SHA256)对令牌进行认证,而 RS256 用 RSA 私钥(RSASSA-PKCS1-v1_5 配合 SHA-256)对令牌签名。这决定了谁能生成令牌、密钥如何分发,以及密钥泄露后会发生什么。

概览

HS256RS256
算法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 令牌。

Node.js:同一载荷的 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,而无需让所有系统在同一天同时切换:

  1. 在 KMS 或 HSM 中创建密钥对,并在 JWKS 中发布公钥,附上新的 kid 和 alg。
  2. 让验证方同时支持两者:按 kid 选择密钥,并把每个密钥绑定到一种算法(共享密钥绑定 HS256,新密钥绑定 RS256)。切勿让同一次验证调用对同一份密钥材料同时接受两种算法。
  3. 使用新的 kid 开始签发非对称令牌;已有的 HS256 令牌在过期之前仍能通过验证。
  4. 等最长的 HS256 令牌有效期过去后,从每个验证方中删除共享密钥。真正的安全收益来自这一步。
  5. 只要旧密钥有任何泄露的可能,无论如何都要轮换它,因为在验证方停止接受 HS256 令牌之前,持有该密钥的攻击者仍能签发这类令牌。

参考资料

全部对比