3. JWT 签名算法:HMAC、RSA 与 ECDSA 原理与选择
JWT 的核心安全机制在于签名。签名算法决定了 Token 的防篡改能力和信任模型。本章深入剖析三种主流签名算法:HMAC(对称)、RSA(非对称) 和 ECDSA(椭圆曲线),帮助你在分布式系统中做出正确选择。
3.1 签名算法概览与核心差异
JWT 标准(RFC 7519)定义了多种签名算法,通过 alg 头部字段标识。下表对比了三种最常用的算法族:
| 特性 | HMAC(HS256/384/512) | RSA(RS256/384/512) | ECDSA(ES256/384/512) |
|---|---|---|---|
| 密钥类型 | 对称密钥(单一密钥) | 非对称密钥对(公钥 + 私钥) | 非对称密钥对(公钥 + 私钥) |
| 签名速度 | 极快 | 较慢(私钥签名) | 快(私钥签名) |
| 验证速度 | 极快 | 较快(公钥验证) | 快(公钥验证) |
| 密钥分发 | 困难(需安全共享密钥) | 容易(公钥可公开) | 容易(公钥可公开) |
| 签名长度 | 固定(取决于哈希输出) | 较长(与密钥长度相关) | 较短(与密钥长度相关) |
| 典型应用 | 单体应用、内部微服务 | 开放 API、第三方集成 | 移动端、IoT、高并发场景 |
选择建议: 如果所有验证方都在你控制的安全网络内,HMAC 性能最优;如果需要将验证能力开放给第三方或前端,必须使用非对称算法(RSA 或 ECDSA)。
3.2 HMAC 对称签名:简单高效的共享秘密
HMAC(Hash-based Message Authentication Code)使用同一个密钥进行签名和验证。签名方和验证方必须事先共享这个密钥。
3.2.1 工作原理
- 准备数据: 将 JWT 的 Header 和 Payload 分别进行 Base64URL 编码,用点号连接。
- 计算签名: 使用 HMAC 算法(如 HMAC-SHA256)和共享密钥对上述数据进行哈希运算,生成签名。
- 验证: 接收方使用相同的密钥和算法重新计算签名,比对是否一致。
3.2.2 代码示例(Node.js)
const crypto = require('crypto');
// 共享密钥(必须安全存储)
const SECRET_KEY = 'your-256-bit-secret';
// 签名
function signJWT(header, payload) {
const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
const data = `${headerBase64}.${payloadBase64}`;
const signature = crypto.createHmac('sha256', SECRET_KEY).update(data).digest('base64url');
return `${data}.${signature}`;
}
// 验证
function verifyJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) return false;
const data = `${parts[0]}.${parts[1]}`;
const expectedSig = crypto.createHmac('sha256', SECRET_KEY).update(data).digest('base64url');
return expectedSig === parts[2];
}
安全警告: HMAC 密钥必须通过带外安全通道(如密钥管理服务 KMS、Vault)分发。切勿将密钥硬编码在代码仓库或前端代码中。一旦密钥泄露,所有 Token 均可被伪造。
3.3 RSA 非对称签名:公钥验证的信任基石
RSA 使用一对密钥:私钥 用于签名,公钥 用于验证。私钥必须严格保密,公钥可以安全地分发给任何需要验证 Token 的服务。
3.3.1 工作原理
- 签名方(认证服务): 使用私钥对 JWT 数据进行 RSA 签名。
- 验证方(资源服务): 使用公钥验证签名,无需知道私钥。
- 密钥分发: 公钥可通过 JWKS(JSON Web Key Set)端点公开获取。
3.3.2 代码示例(Node.js)
const crypto = require('crypto');
// 生成密钥对(生产环境使用 KMS 或安全存储)
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// 签名(使用私钥)
function signRSA(header, payload) {
const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
const data = `${headerBase64}.${payloadBase64}`;
const signature = crypto.sign('sha256', Buffer.from(data), privateKey).toString('base64url');
return `${data}.${signature}`;
}
// 验证(使用公钥)
function verifyRSA(token) {
const parts = token.split('.');
if (parts.length !== 3) return false;
const data = `${parts[0]}.${parts[1]}`;
return crypto.verify('sha256', Buffer.from(data), publicKey, Buffer.from(parts[2], 'base64url'));
}
3.4 ECDSA 椭圆曲线签名:高性能的现代选择
ECDSA(Elliptic Curve Digital Signature Algorithm)基于椭圆曲线密码学,在相同安全强度下,密钥更短、签名更小、计算更快。推荐使用 P-256(ES256) 曲线。
3.4.1 与 RSA 的对比优势
| 对比维度 | RSA-2048 | ECDSA P-256 |
|---|---|---|
| 密钥长度 | 2048 位 | 256 位 |
| 签名长度 | 256 字节 | 64 字节 |
| 签名速度 | 较慢 | 快 3-5 倍 |
| 验证速度 | 较快 | 快 2-3 倍 |
| 安全强度 | 112 位 | 128 位 |
3.4.2 代码示例(Node.js)
const crypto = require('crypto');
// 生成 ECDSA P-256 密钥对
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'P-256',
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// 签名
function signECDSA(header, payload) {
const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
const data = `${headerBase64}.${payloadBase64}`;
const signature = crypto.sign('sha256', Buffer.from(data), privateKey).toString('base64url');
return `${data}.${signature}`;
}
// 验证
function verifyECDSA(token) {
const parts = token.split('.');
if (parts.length !== 3) return false;
const data = `${parts[0]}.${parts[1]}`;
return crypto.verify('sha256', Buffer.from(data), publicKey, Buffer.from(parts[2], 'base64url'));
}
3.5 签名算法选择决策流程
下图展示了在分布式系统中选择 JWT 签名算法的决策路径:
3.6 算法选择实战建议
生产环境推荐:
- 内部微服务(服务间通信): 优先使用 HMAC(HS256),性能最优,密钥通过服务网格或 K8s Secret 分发。
- 开放 API / 第三方集成: 必须使用 RSA(RS256) 或 ECDSA(ES256),公钥通过 JWKS 端点暴露。
- 移动端 / IoT / 高并发: 强烈推荐 ECDSA(ES256),签名短、验证快、节省带宽。
- 遗留系统兼容: 如果已有 RSA 基础设施,继续使用 RS256;新系统一律选择 ES256。
3.7 常见安全陷阱与避坑指南
| 陷阱 | 风险 | 解决方案 |
|---|---|---|
| alg: none | 攻击者将 alg 改为 none,绕过签名验证 | 服务端强制校验 alg 字段,拒绝 none 算法 |
| 密钥硬编码 | 密钥泄露,Token 可被任意伪造 | 使用环境变量、KMS、Vault 管理密钥 |
| 弱密钥长度 | 暴力破解风险 | HMAC 使用 ≥256 位密钥;RSA 使用 ≥2048 位;ECDSA 使用 P-256 或更高 |
| 公钥信任缺失 | 攻击者替换公钥,伪造 Token | 公钥通过 HTTPS 获取,并验证 JWKS 端点证书 |
| 算法混淆攻击 | 将 RS256 Token 用 HS256 公钥验证 | 服务端固定算法白名单,不依赖客户端声明 |
核心原则: 永远不要信任客户端传入的
alg 字段。服务端必须硬编码允许的算法列表,并在验证时严格匹配。这是防止 JWT 签名绕过攻击的第一道防线。