JWT 安全使用 8 项最佳实践
JWT(JSON Web Token)是过去十年最普及的无状态身份凭证格式。它把用户身份与权限信息序列化为一个紧凑的字符串,由签发方签名、由资源方校验,解决了分布式系统下传统 Session 难以横向扩展的痛点。但 JWT 的简单并不代表安全——alg=none 漏洞、弱密钥、误用对称算法、payload 放敏感数据、缺乏撤销机制等问题层出不穷。本文整理 8 项 JWT 安全使用的最佳实践,覆盖签名与加密的本质区别、HS 与 RS 选型、密钥轮换、过期策略、refresh token 与 session 的取舍、以及容易踩坑的常见错误,帮你在 2026 年安全地使用 JWT。
一、JWS 与 JWE:签名与加密本质不同
很多开发者把 JWT 当成"加密过的 token",这是严重的误解。日常使用的 JWT 实际上是 JWS(JSON Web Signature),结构是 header.payload.signature,前两段是 base64url 编码的明文 JSON,第三段是签名。任何拿到 token 的人都可以解码 payload,看到里面所有的字段。签名只能保证"这段内容没被篡改",无法保证"内容不被读取"。
真正能加密 payload 的是 JWE(JSON Web Encryption),结构更复杂、五段式:header.encryptedKey.iv.ciphertext.authTag。客户端用对方公钥加密一个对称密钥,再用对称密钥加密 payload。资源方用私钥解出对称密钥,再解出明文。JWE 在跨组织传递包含医疗、金融等敏感信息的场景使用,但开销远大于 JWS。最佳实践是:JWS 携带最少非敏感字段,敏感数据通过 sub 反查后端,需要传递敏感字段时才用 JWE。本站的 AES vs RSA 一文详细讨论了对称与非对称加密的区别。
二、HS256 vs RS256:选型决策框架
JWT 支持多种签名算法。HS256 使用 HMAC-SHA256 对称签名,签发与验证用同一个密钥;RS256 使用 RSA-SHA256 非对称签名,签发用私钥、验证用公钥;ES256 使用 ECDSA,性能更好;EdDSA(Ed25519)是 2026 年的推荐算法,安全性与性能俱佳。
选型核心看部署架构。如果签发方与验证方是同一个服务(小型单体应用),HS256 简单高效。如果签发方是统一的认证中心,多个微服务需要独立校验 token(典型微服务架构),必须用 RS256 或 EdDSA——公钥可以安全地分发到任意服务,私钥只在认证中心保管。永远不要把 HS256 的密钥放进多个服务,密钥泄露面会指数级扩大。EdDSA 的公钥更小、签名更快,是新项目的优选。
三、密钥强度与轮换
HS256 的密钥必须足够长。RFC 7518 要求至少 256 位(32 字节),但实际部署经常看到 8 位 admin 这种弱密钥,攻击者用字典爆破几秒就能破解。生产环境密钥应该使用密码学随机数生成器产生,至少 64 字节。可以使用本站的 随机字符串生成器生成符合要求的密钥。
密钥轮换是被严重忽视的实践。绝大多数公司的 JWT 密钥从上线第一天用到现在,几年都没换过。一旦密钥泄露,所有历史 token 都可信。正确做法是设计 kid(key id)字段:header 中带 kid,验证方根据 kid 选择对应公钥;签发方维护多版本密钥,定期生成新版本,旧版本逐步淘汰;JWKS endpoint(/.well-known/jwks.json)公开当前所有有效公钥,验证方定期拉取。轮换周期建议每 90 天,紧急情况下可立即吊销特定 kid。
四、过期时间与生效时间
JWT 的 exp(expiration)与 nbf(not before)是关键安全字段。exp 太长,token 泄露后损失放大;exp 太短,频繁刷新增加授权服务器压力。经验值:access_token 5 到 15 分钟,refresh_token 数天到数周,长生命 API key 仅用于机器对机器、且 scope 极小。
nbf 字段防止 token 被提前签发使用,对延迟激活的场景有用。iat(issued at)记录签发时间,可用于实现"密码修改后所有旧 token 失效"——把 password_changed_at 与 iat 比较即可。注意时钟漂移问题,验证时应允许 30 秒到 1 分钟的容忍区间,否则跨机房调用会偶尔报错。Redis 缓存或 NTP 同步是减少漂移的常规手段。
五、Refresh Token 与会话管理
短 access_token 提升安全性,但用户不能每 5 分钟重新登录。Refresh token 是解法:长生命的凭证,专门用于换新的 access_token,不直接访问业务 API。客户端保存 refresh token,access_token 过期前用 refresh_token 静默续签,用户无感。
refresh token rotation 是高价值实践:每次使用 refresh token 都换发新的 refresh token、使旧的立即失效。如果攻击者偷到了 refresh token 并使用了,真用户下次使用旧 token 时会被检测到(已失效),整个 token 家族立即吊销,强制重新登录。配合 Redis 维护 refresh token 家族 ID 与版本号,可以做到分钟级的入侵检测。本站 OAuth 2.0 完全指南 详细讨论了 refresh token 的存储与使用。
六、四个最常见的 JWT 漏洞
第一个是 alg=none 漏洞:早期 JWT 库允许 header 指定 alg=none 跳过签名验证。攻击者把 header 改成 alg=none、签名段留空,通过校验。修复方法是验证时显式指定接受的算法白名单,绝不信任 header 中声明的算法。
第二个是 RS256 转 HS256 攻击:服务端用 RS256 私钥签名,验证时简单复用一个 verify 方法。攻击者把 token 的 alg 改为 HS256,把公钥当 HMAC 密钥签名(公钥是公开的),通过校验。修复同样是显式指定算法。第三个是弱密钥爆破:HS256 密钥小于 32 字节时,工具可以离线爆破。第四个是 kid 注入:kid 字段直接拼到文件路径或 SQL 里,导致路径穿越或 SQL 注入。永远把 kid 当作不可信输入,严格白名单。可以参考本站的 密码安全最佳实践了解密钥管理通用原则。
七、Payload 应该放什么、不该放什么
JWS 的 payload 是公开可读的,所以 payload 设计的核心原则是:放最少的、非敏感的、无密码的信息。推荐字段:iss(签发方)、sub(用户 ID)、aud(受众)、exp(过期)、iat(签发时间)、jti(token ID)、scope(权限)、roles(角色,如果数量少且固定)。
不应该放:密码(任何形式)、身份证号、银行卡号、医疗诊断、地址、电话等 PII。这些信息一旦在日志、监控、APM 中被记录,就形成数据泄露事件。也不要塞超过 8KB 的内容——HTTP header 大小有限制,超过会被网关拒绝;payload 越大,每次请求带宽消耗越大;JWT 终究不是数据库,不要把它当用户档案用。如果客户端确实需要展示用户名、头像,sub 反查后端用户接口,配合 CDN 缓存即可。
八、JWT 撤销与黑名单
JWT 的最大缺点是难以即时撤销。一旦签发,在 exp 之前都有效,无法主动让单个 token 失效。这与传统 session 的"删 session_id 即下线"形成鲜明对比。
有几种撤销方案。黑名单:维护一个 Redis Set,存储已撤销 token 的 jti,校验时查询;缺点是引入了状态,但 Redis 查询代价低。短过期 + refresh rotation:access_token 5 分钟过期,refresh token 撤销后下次刷新就失败;最长损失等于 access_token 有效期,多数场景可接受。Token version:每个用户维护一个 version 字段,签发时写入 payload,校验时比对;用户改密、强制下线时升版本;这是 Sentry 等系统的方案。综合权衡,对一般业务推荐"短 access_token + refresh rotation",对金融与管理后台推荐"短 access_token + 黑名单 + version"。
常见问题
JWT 和 Session 有什么本质区别?
Session 是有状态的,服务端存储会话数据,客户端只持有 session_id;JWT 是无状态的,客户端携带完整凭证,服务端只校验签名。Session 易于撤销但难以横向扩展;JWT 易于扩展但难以即时撤销。中大型分布式系统常用 JWT,对安全性要求极高的金融场景常用 Session 或者 JWT + 黑名单的混合方案。
HS256 和 RS256 怎么选?
HS256 是对称签名,签发与验证用同一个密钥,适合签发与验证都在同一个可信服务的场景。RS256 是非对称签名,私钥签发、公钥验证,适合多个微服务都要校验 token 的场景,公钥可以安全地分发。建议默认 RS256 或 EdDSA,HS256 仅在单服务且性能敏感时使用。EdDSA 在 2026 年是更优选择,公钥更小、签名更快、安全性更高。
JWT 的过期时间设多长合适?
access_token 5 到 15 分钟为宜,平衡安全与性能。refresh_token 数天到数周,配合 rotation 与家族追踪。绝对不要设置数月级别的 access_token——一旦泄露损失巨大。永久 token 必须限制在机器对机器场景且 scope 极小,并存储在 KMS 等专门的密钥管理服务里,定期审计与轮换。
JWT 真的能放敏感数据吗?
默认的 JWS 只是 base64 编码,任何拿到 token 的人都能读取 payload,所以不要放密码、身份证号、医疗信息等敏感字段。如果必须放,使用 JWE 进行加密。最佳实践是 payload 只放 sub、scope、exp 等最少字段,敏感数据用 sub 反查后端。这也避免了 token 过大、日志泄露等次生问题。
JWT 怎么实现强制下线?
JWT 本身无状态,强制下线需要额外机制。常用方案:黑名单在 Redis 维护已撤销 token 列表,校验时查询;短过期加 refresh token rotation,最长损失等于 access_token 有效期;维护用户级 token version,签发时写入 payload,撤销时升版本。多数场景方案二已足够,金融场景推荐多重组合。
相关工具
- 随机字符串生成器 — 生成强密钥
- SHA256 哈希工具 — 验证签名计算
- Base64 编解码 — 解码 JWT payload 查看字段