先说结论:HS256 是对称加密,签名验签用同一密钥;RS256 是非对称加密,私钥签名、公钥验签,密钥管理更安全但计算开销更大。
- 适合:HS256 适合单服务或强内网闭环场景,RS256 适合微服务、跨组织、多验签方场景
- 重点看:密钥泄露风险——HS256 任一验签节点失陷即全量密钥暴露,RS256 私钥仅需驻留授权服务器
- 别忽略:JWT 的 Header 和 Payload 只是 Base64 编码而非加密,任何人都能解码查看内容,签名才是防篡改核心
选型决策思路
这不是命令操作类问题,而是算法选型决策。按以下思路判断:
第一步:确认验签方数量
如果只有认证服务自己验签,HS256 足够;如果有 API 网关、多个微服务都需要独立验签,RS256 更合适。
第二步:评估密钥管理成本
HS256 需要在所有验签服务间同步同一 secret,任何一处泄露都要全局轮换;RS256 只需保护私钥,公钥可通过 JWKS 端点动态发布。
第三步:检查现有基础设施
如果已使用 Auth0、Keycloak 等身份提供商,它们默认支持 RS256;如果是自建简单认证服务,HS256 配置更直接。
核心原理差异
两种算法的根本差异在于密钥体系不同。HS256 使用 HMAC with SHA-256,签名和验签用同一个密钥,属于对称加密范式。服务器生成 Token 时用 secret 计算签名,验签时也用同一个 secret 重新计算并比对。
RS256 使用 RSA with SHA-256,属于非对称加密。私钥仅用于签名,必须严格控制在授权服务器;公钥用于验签,可以安全分发给任何需要验证 Token 的服务。这种设计将密钥攻击面从 N 个服务节点压缩至 1 个高安保节点。
JWT 本身由三部分组成:Header(声明算法)、Payload(业务数据)、Signature(防篡改签名)。Header 和 Payload 只是 Base64URL 编码,任何人都能解码查看,签名才是保证数据未被篡改的关键。服务器收到 Token 后,用同样的算法和密钥重新计算签名,与 Token 中的第三部分比较,一致则说明未被篡改。
代码实现与配置
步骤一:生成密钥对(RS256 场景)
使用 OpenSSL 生成 RSA 密钥对:
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem私钥 private.pem 仅部署在认证服务,公钥 public.pem 可分发给所有验签服务或通过 JWKS 端点发布。
步骤二:Python 实现示例(PyJWT)
安装依赖:
pip install PyJWTHS256 签名与验签:
import jwt
SECRET_KEY = "your-secret-key"
payload = {"user_id": 123}
# 签名
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
# 验签(必须指定算法)
try:
data = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.InvalidTokenError:
# 验签失败处理
passRS256 签名与验签:
import jwt
with open("private.pem", "r") as f:
private_key = f.read()
with open("public.pem", "r") as f:
public_key = f.read()
# 签名
token = jwt.encode(payload, private_key, algorithm="RS256")
# 验签
data = jwt.decode(token, public_key, algorithms=["RS256"])步骤三:Java 实现示例(jjwt)
Maven 依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>HS256 签名与验签:
import io.jsonwebtoken.*;
import javax.crypto.spec.SecretKeySpec;
String secret = "your-secret-key";
Key key = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
// 签名
String token = Jwts.builder()
.setSubject("123")
.signWith(key, SignatureAlgorithm.HS256)
.compact();
// 验签
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);步骤四:密钥轮换计划
RS256 支持密钥轮换而无需重新部署所有验签服务,可通过 JWKS 端点动态发布新公钥。HS256 轮换密钥需要所有持有 secret 的服务同步更新。
验证与排查
检查点一:Token 结构
解码 Token 的 Header 部分,确认 alg 字段为预期值(HS256 或 RS256)。可使用在线 JWT 解码工具或命令行:
echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d检查点二:篡改测试
修改 Payload 中的任意字段(如 role 从 user 改为 admin),保持 Header 和 Signature 不变,用验签服务验证。预期结果应为验签失败,Token 被拒绝。
检查点三:日志审计
查看认证服务日志,确认签名生成时使用的算法和密钥 ID(RS256 场景通常有 kid 字段)。查看验签服务日志,确认验签失败时有明确的签名验证错误记录。
检查点四:公钥分发(RS256 场景)
访问 JWKS 端点(如果有),确认公钥可正常获取且格式正确。验签服务应能缓存并定期刷新公钥。
常见风险与坑
坑一:误以为 Payload 是加密的
JWT 的 Payload 只是 Base64 编码,不是加密。敏感信息(如密码、手机号)不应放入 Payload,即使加了签名,内容仍可被任何人解码查看。
坑二:不校验 alg 字段
如果服务端不校验 Token Header 中的 alg 值,攻击者可能将其改为 none 绕过签名验证。验签代码必须明确指定期望的算法,如 Python 中的 algorithms 参数。
坑三:HS256 密钥硬编码或日志泄露
HS256 的 secret 等同于根证书,一旦泄露整个认证体系崩塌。不要将 secret 写入代码仓库、配置文件版本控制或日志中。建议使用环境变量或密钥管理服务。
坑四:RS256 公私钥混淆
私钥用于签名,公钥用于验签。配置时不要搞反,否则验签永远失败。私钥必须严格保护,公钥可以公开分发。
坑五:遗留系统兼容性
部分旧系统或库可能不支持 RS256,此时可考虑 HS256,但需接受密钥管理风险。处理不支持 RS256 的遗留应用程序时,HS256 是可接受的替代方案。
坑六:跨语言调用算法差异
同一算法名在不同 JWT 库里的实现可能有差异。例如 PS256 和 RS256 都是 RSA 家族,但填充模式不同。跨语言调用时(如 Node.js 签发、Go 验证)需测试算法兼容性。
参考标准
- RFC 7519 - JSON Web Token (JWT)
- RFC 7515 - JSON Web Signature (JWS)
- Auth0 Documentation - JWT Signing Algorithms