JWT 的 Header 里有个 alg 字段,填 HS256 还是 RS256?很多人直接用库的默认值,从没想过这个问题。选错了,轻则性能浪费,重则安全漏洞。本文把三类签名算法的原理、适用场景和选型决策讲清楚。
JWT 签名算法分类
JWT 规范定义了三类签名算法:
| HMAC | HS256 / HS384 / HS512 | 对称密钥 | 单体应用、内部服务 |
| RSA | RS256 / RS384 / RS512 | 非对称密钥(公钥+私钥) | 微服务、第三方授权 |
| ECDSA | ES256 / ES384 / ES512 | 非对称密钥(公钥+私钥) | 移动端、物联网 |
| None | none | 无密钥 | ❌ 不应该使用 |
还有 PS256(RSASSA-PSS)系列,是 RSA 的概率性签名变体,安全性更高但库支持较少,实际项目中不太常见。
HMAC 对称签名(HS256)
HS256 是最常见的 JWT 算法。签发和验证用同一个密钥——这就是"对称"的含义。
工作原理:
签名 = HMAC-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
用 Node.js 的 crypto 模块手动实现:
const crypto = require('crypto')
function signHS256(header, payload, secret) {
const data = Buffer.from(
base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload))
)
return crypto.createHmac('sha256', secret).update(data).digest('base64url')
}
function verifyHS256(token, secret) {
const parts = token.split('.')
const data = parts[0] + '.' + parts[1]
const expected = crypto.createHmac('sha256', secret).update(data).digest('base64url')
// 用 timingSafeEqual 防止时序攻击
const a = Buffer.from(expected)
const b = Buffer.from(parts[2])
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
用 jsonwebtoken 库更简单:
const jwt = require('jsonwebtoken')
const token = jwt.sign(
{ sub: 'user-123', role: 'admin' },
'my-256-bit-secret-key-aaaaaa',
{ algorithm: 'HS256', expiresIn: '1h' }
)
const decoded = jwt.verify(token, 'my-256-bit-secret-key-aaaaaa')
优点:计算快,实现简单,一个密钥搞定。
缺点:签发方和验证方共享密钥。任何持有密钥的服务都能签发 Token,无法区分"谁签的"。
适用场景:单体应用(自己签自己验)、内部微服务间通信(可信环境)。
密钥要求:至少 256 位(32 字节)。用 crypto.randomBytes(32).toString('hex') 生成。
RSA 非对称签名(RS256)
RS256 用私钥签名、公钥验证。签发方持有私钥,验证方只需要公钥。
工作原理:
签名 = RSA-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
验证 = RSA-SHA256-Verify(data, signature, publicKey)
生成 RSA 密钥对:
# 生成私钥
openssl genrsa -out private.pem 2048
# 从私钥导出公钥
openssl rsa -in private.pem -pubout -out public.pem
Node.js 代码:
const crypto = require('crypto')
const privateKey = require('fs').readFileSync('private.pem')
const publicKey = require('fs').readFileSync('public.pem')
// 签发(用私钥)
const token = jwt.sign({ sub: 'user-123' }, privateKey, {
algorithm: 'RS256',
expiresIn: '1h'
})
// 验证(用公钥)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] })
优点:公钥可以公开分发,任何服务都能验证 Token,但只有签发方能签发。解决了"谁签的"问题。
缺点:签名和验证速度比 HMAC 慢一个数量级。RSA-2048 密钥体积大(PEM 文件约 1.7KB)。
适用场景:微服务架构(认证中心签发,多个业务服务验证)、OAuth2/OpenID Connect、第三方授权。
ECDSA 椭圆曲线签名(ES256)
ES256 用椭圆曲线密码学(ECC),同样是私钥签名、公钥验证,但密钥更短、速度更快。
RSA vs ECDSA 密钥长度对照:
| 128 位 | 2048 | 256 | RS256 / ES256 |
| 192 位 | 3072 | 384 | RS384 / ES384 |
| 256 位 | 15360 | 521 | RS512 / ES512 |
同样的安全等级,ECDSA 的密钥短一个数量级。
生成 ECDSA 密钥对:
# 生成 ECDSA 私钥(prime256v1 = ES256 曲线)
openssl ecparam -genkey -name prime256v1 -noout -out ec-private.pem
# 导出公钥
openssl ec -in ec-private.pem -pubout -out ec-public.pem
Node.js 代码:
const privateKey = require('fs').readFileSync('ec-private.pem')
const publicKey = require('fs').readFileSync('ec-public.pem')
// 签发
const token = jwt.sign({ sub: 'user-123' }, privateKey, {
algorithm: 'ES256',
expiresIn: '1h'
})
// 验证
const decoded = jwt.verify(token, publicKey, { algorithms: ['ES256'] })
优点:密钥短、签名短、签名速度比 RSA 快 3-5 倍。适合带宽受限场景。
缺点:实现比 RSA 复杂,库的兼容性不如 RSA。如果 ECDSA 的随机数源有问题(如重复随机数),会导致私钥泄露。
适用场景:移动端认证、物联网设备、对 Token 体积敏感的 API。
alg: none —— 绝对不要用
JWT 规范允许 alg: none,表示不签名。Token 只有 Header 和 Payload 两段,没有 Signature。
// 签发无签名 Token(危险!)
const token = jwt.sign({ sub: 'user-123' }, '', { algorithm: 'none' })
// eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTEyMyJ9.
这个算法存在于规范中是为了开发调试方便,但生产环境绝对不能用。很多 JWT 安全漏洞都和 alg: none 有关——攻击者把 alg 改成 none,服务端不校验算法就直接信任 Payload。
防御方法:验证 Token 时显式指定允许的算法:
// 错误:不指定算法,可能被 alg: none 绕过
const decoded = jwt.verify(token, publicKey)
// 正确:白名单指定算法
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256', 'ES256'] // 不包含 none
})
选型决策树
是否需要多个服务验证 Token?
├── 否 → HS256(对称密钥,简单快速)
└── 是 → 是否对 Token 体积/性能敏感?
├── 否 → RS256(RSA 非对称,生态最成熟)
└── 是 → ES256(ECDSA 非对称,密钥短速度快)
实际项目中 90% 的团队选 HS256,因为单体应用或内部服务间不区分签发方和验证方。一旦要做第三方授权或开放 API,就应该切 RS256。ES256 在国内用得少,但在 Google、Cloudflare 等公司的体系中很常见。
算法速查表
| HS256 | HMAC | 共享密钥 | 快 | 快 | 推荐 |
| HS384 | HMAC | 共享密钥 | 快 | 快 | 推荐 |
| HS512 | HMAC | 共享密钥 | 快 | 快 | 推荐 |
| RS256 | RSA | 公钥+私钥 | 慢 | 中 | 推荐 |
| RS384 | RSA | 公钥+私钥 | 慢 | 中 | 推荐 |
| RS512 | RSA | 公钥+私钥 | 慢 | 中 | 推荐 |
| ES256 | ECDSA | 公钥+私钥 | 中 | 快 | 推荐 |
| ES384 | ECDSA | 公钥+私钥 | 中 | 快 | 推荐 |
| ES512 | ECDSA | 公钥+私钥 | 中 | 快 | 推荐 |
| none | None | 无 | – | – | 不安全 |
在线验证
理解算法最好的方式是看真实 Token 的 Header 差异。可以在 盘子工具站 JWT 解析工具 上做这种对比——粘贴不同算法签发的 Token,观察 Header 卡片中 alg 字段的变化。用 HS256 签发的 Token,Header 显示 "alg": "HS256";用 RS256 签发的,显示 "alg": "RS256"。
工具的算法参考表直接列出了 10 种 JWT 算法的类型、说明和安全性标注。如果 Token 用的是 alg: none,工具会显示醒目的橙色"无签名算法(alg=none)"警告——你可以构造一个 alg: none 的 Token 粘贴进去看看效果,理解为什么生产环境不能用。
工具的所有解码在浏览器本地完成,不上传服务器。在选型阶段,可以把不同算法签发的 Token 都粘贴进去,对比各段的长度差异——RSA 签名的 Token 比 HMAC 的长不少,ECDSA 居中。这对 Token 在 URL 或 Header 中传输的体积评估很有参考价值。




