先记住这个答案
HS256使用同一密钥签名和验证,密钥即全部信任根。生成应使用密码学安全随机数产生至少256位密钥,存储于KMS、密钥管理服务或环境变量,禁止写入源码。轮换需在JWT头部携带kid标识密钥版本,服务端按kid查找当前密钥列表,支持旧密钥宽容验证新密钥签发。硬编码风险包括代码仓库泄露、人员离职泄露、无法快速更替密钥;一旦密钥泄露,所有已签发JWT都可被伪造。
- 密钥至少256位且用密码学安全随机源生成
- 密钥应存KMS或环境变量,禁止进代码库
- 用kid标识密钥版本,并支持平滑轮换
密钥生成与硬度要求
HS256算法要求客户端和服务端共享一个对称密钥。密钥强度直接决定暴力破解难度,官方建议至少256位(32字节)随机值。生成时必须使用密码学安全伪随机数生成器,例如Node.js的crypto.randomBytes或openssl rand -base64 32,绝对不能用Math.random()或可预测的短字符串。
存储位置优先选择云KMS(如AWS KMS、GCP KMS),密钥加密后存于服务配置,运行时解密到内存。没有KMS时可用环境变量,但必须限制读取权限并跟踪变更。硬编码在代码中会使密钥出现在版本历史、日志和备份中,等于任何读到源码的人都能伪造令牌。
带kid的平滑轮换实操
假设服务现有密钥old-key,计划轮换到new-key。签发新JWT时用新密钥,头部填写kid: "new-key"(Claims Set之外)。服务端持有一个密钥映射表,验证时读取kid查找对应密钥。但旧令牌可能还没过期,需要在过渡期同时保留新、旧密钥。
操作上:先添加新密钥,所有旧令牌的kid是"old-key"仍能验证。然后签发新令牌全部使用新密钥。等所有旧令牌exp过去或平均寿命结束后,移除旧密钥。如果必须立即生效,可以强制旧令牌立即过期并重新登录,代价是中断现有会话。
密钥管理的失效条件
密钥轮换并非自动解决所有问题:如果攻击者已窃取当前密钥,在旧密钥被停用前,任何未过期的旧JWT仍可被验证,所以轮换后应立即停用旧密钥并吊销所有由旧密钥签发的令牌。没有kid的JWT在轮换时只能接受全部候选密钥逐一尝试,开销大且易被混淆。
分布式环境中,如果实例各自持有不同密钥副本,轮换时同步滞后会导致验证失败。因此建议使用统一的密钥管理服务或配置中心,并确保所有实例在启动时加载最新密钥版本。轮换的频率和审计日志应作为运维基线,而不是随意操作。
容易答错的地方
- 密钥并非越长越安全
- HS256密钥长度超过256位并不增加安全性,且HMAC会先哈希密钥,因此超长密钥对计算开销的影响可忽略。建议使用至少256位的密码学随机密钥,无需刻意使用超长密钥,但生成时必须确保熵足够。
- 用UUID或简单字符串作为密钥
- UUID只有122位随机熵,低于推荐的256位,且部分旧版本可预测;简单字符串如
secret123易被字典攻击。必须用密码学随机字节,推荐生成Base64或Hex格式,并定期轮换。
面试官还会怎么问?
如果密钥泄露了,除了换密钥,还需要做什么?
立即吊销所有使用旧密钥签发的JWT,通常可以递增版本号使旧令牌全局失效,或维护一个黑名单直到过期。同时检查审计日志,对敏感操作进行安全响应。
验证时每次都要查密钥库吗?
不必,可以缓存密钥映射,但需要设置短期TTL并监听KMS的密钥更新事件。缓存不当会导致轮换后旧实例仍在用旧密钥校验新JWT,造成间歇性401。
环境变量存储vs KMS哪个更适合中小团队?
环境变量适合快速原型和低合规场景,但缺乏细粒度访问控制和轮换审计。KMS成本较高但提供密钥版本和加密,降低泄露责任。至少使用环境变量并配合密钥管理工具。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。