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 工作原理

  1. 准备数据: 将 JWT 的 Header 和 Payload 分别进行 Base64URL 编码,用点号连接。
  2. 计算签名: 使用 HMAC 算法(如 HMAC-SHA256)和共享密钥对上述数据进行哈希运算,生成签名。
  3. 验证: 接收方使用相同的密钥和算法重新计算签名,比对是否一致。

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 工作原理

  1. 签名方(认证服务): 使用私钥对 JWT 数据进行 RSA 签名。
  2. 验证方(资源服务): 使用公钥验证签名,无需知道私钥。
  3. 密钥分发: 公钥可通过 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 签名算法的决策路径:

JWT 签名算法选择决策流程 开始选择签名算法 验证方是否完全可控? 是 否 HMAC(HS256) 非对称算法 特点 单一密钥,性能极佳 密钥分发困难,需安全通道 高并发/移动端? 是 否 ECDSA(ES256) RSA(RS256) 推荐:新系统优先选择 ECDSA P-256(ES256) 兼顾安全、性能与签名长度,适合分布式微服务架构

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 签名绕过攻击的第一道防线。