令牌生成安全:随机数、签名算法与密钥管理

令牌生成,说白了就是给用户发一张“通行证”。这张证怎么发、用什么材料造、谁来签字盖章,直接决定了系统的安全水位。我见过太多团队在令牌生成上栽跟头——不是随机数不够随机,就是签名算法选错,再不然密钥管理一塌糊涂。今天咱们就把这三个核心问题掰开揉碎讲清楚。

安全的随机数生成:别让“随机”变成“可预测”

随机数生成是令牌安全的基石。你想想看,如果攻击者能预测出下一个令牌的值,那整个认证体系就形同虚设。我在项目中遇到过一家公司,他们用 Math.random() 生成 OAuth 2.0 的 access ,结果被安全审计直接打回——因为 JavaScript 的 Math.random() 是伪随机数生成器(PRNG),种子空间有限,完全可预测。

⚠️ 核心原则: 令牌生成必须使用密码学安全的伪随机数生成器(CSPRNG)。普通语言的随机函数(如 Java 的 java.util.Random、Python 的 random 模块)都不合格。

各语言推荐做法如下:

语言 安全做法 不安全做法
Java java.security.SecureRandom java.util.Random
Python secrets._hex() random.getrandbits()
Node.js crypto.randomBytes() Math.random()
Go crypto/rand math/rand

我个人习惯在生成 access 时,至少使用 32 字节(256 位)的随机数。举个例子:

// Node.js 安全生成 access 
const crypto = require('crypto');
const  = crypto.randomBytes(32).toString('hex');
// 输出类似:a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0
💡 小技巧: 我曾经在审计时发现,有些团队用 UUID 作为令牌。UUID 虽然看起来随机,但部分版本(如 UUIDv1)包含时间戳和 MAC 地址,可预测性很高。如果非要用 UUID,请使用 UUIDv4,并且确保底层用的是 CSPRNG。

签名算法选择:RS256 vs HS256,选错就是灾难

JWT 的签名算法,最常用的就是 RS256 和 HS256。很多人觉得“反正都能签名,随便选一个就行”——嗯,这里面的坑可不少。

先看一张对比表:

特性 RS256(非对称) HS256(对称)
密钥类型 公钥/私钥对 共享密钥
签名方 私钥持有者(通常是授权服务器) 任何知道密钥的一方
验证方 公钥持有者(任何资源服务器) 任何知道密钥的一方
密钥分发 公钥可公开,私钥保密 密钥必须在所有验证方之间共享
密钥泄露影响 私钥泄露可伪造令牌;公钥泄露无影响 密钥泄露后,所有验证方都能伪造令牌
性能 签名慢,验证快 签名和验证都快

为什么我强烈推荐 RS256?

第一,密钥隔离。授权服务器用私钥签名,资源服务器用公钥验证。就算资源服务器被攻破,攻击者拿到公钥也伪造不了令牌。我在项目中见过一个案例:某公司用 HS256,所有微服务共享同一个密钥。结果一个边缘服务被入侵,攻击者直接拿到了密钥,然后伪造了管理员令牌——整个系统沦陷。

第二,密钥轮换方便。RS256 只需要在授权服务器上轮换私钥,资源服务器更新公钥即可。HS256 呢?你得同时更新所有服务,稍有不慎就会导致验证失败。

📌 我的建议: 除非你的系统只有一个授权服务器和一个资源服务器,并且它们之间的通信安全,否则永远选择 RS256。HS256 只适合内部测试或极其简单的场景。

但 RS256 也有坑。我记得有一次排查线上问题,发现 JWT 验证总是失败。查了半天,原来是资源服务器缓存了过期的公钥,而授权服务器已经轮换了密钥对。所以,使用 RS256 时一定要实现公钥的自动刷新机制——通常通过 JWKS(JSON Web Key Set)端点来动态获取。

密钥管理最佳实践:别把钥匙挂在门上

密钥管理是令牌安全中最容易被忽视的环节。我见过有人把私钥直接写在代码里,有人把密钥放在 Git 仓库中,还有人用硬编码的字符串当密钥——这些操作,说白了就是给攻击者送人头。

密钥管理需要遵循几个核心原则:

  • 密钥分级: 签名密钥、加密密钥、存储密钥要分开使用,不要复用。
  • 密钥轮换: 定期更换密钥,建议 90 天轮换一次。发生安全事件时立即轮换。
  • 密钥存储: 使用硬件安全模块(HSM)或密钥管理服务(KMS),比如 AWS KMS、Azure Key Vault、HashiCorp Vault。
  • 密钥访问控制: 最小权限原则,只有授权服务器能访问签名私钥。

下面是一个典型的密钥轮换流程:

  1. 生成新的密钥对,标记为“当前活跃”。
  2. 旧密钥对标记为“过期”,但保留一段时间(比如 24 小时)用于验证已签发的令牌。
  3. 新令牌全部使用新私钥签名。
  4. 资源服务器通过 JWKS 端点获取最新的公钥列表。
  5. 旧密钥过期后,从 JWKS 中移除。
⚠️ 避坑指南: 我曾经在审计时发现,某团队轮换密钥后立即删除了旧密钥。结果还有大量未过期的 JWT 无法验证,导致用户全部掉线。记住:密钥轮换要有“宽限期”,新旧密钥要共存一段时间。

另外,密钥的生成也要讲究。不要自己写代码生成 RSA 密钥对,用标准工具:

# 生成 RS256 私钥(2048 位)
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

# 提取公钥
openssl pkey -in private_key.pem -pubout -out public_key.pem

我个人习惯在生成密钥时,使用 4096 位 RSA 或 ECDSA(比如 ES256)。虽然性能稍慢,但安全性更高。对于高并发场景,ECDSA 的性能优势会更明显。

知识体系总览

下面这张图总结了令牌生成安全的核心逻辑:

令牌生成安全核心逻辑 令牌生成安全 随机数生成 签名算法选择 密钥管理 CSPRNG ≥ 32 字节 RS256(推荐) HS256(慎用) 密钥轮换 KMS/HSM 核心原则 1. 随机数必须使用 CSPRNG 2. 签名算法优先选择 RS256 3. 密钥管理要分级、轮换、安全存储

总结一下:令牌生成安全没有银弹。随机数、签名算法、密钥管理,这三者缺一不可。我见过太多团队只关注其中一个方面,结果在其他环节上出了大问题。记住,安全是一个链条,最薄弱的环节决定了整体安全水平。