JWT 应验证哪些字段?Refresh Token 如何安全轮换?
简化版
JWT 不能只验签名,还要校验:算法(用白名单,不信 header 里的 alg)、签名密钥(多密钥用 kid 选)、iss(发行方)、aud(受众/该 token 是不是发给我这个服务的)、exp(未过期)、nbf(已生效),以及业务声明和用户状态(权限、租户、是否被封禁)。Refresh Token(换取新 access token)应长期但可撤销,采用轮换(rotation)+ 复用检测——每次刷新签发新的、旧的立即失效;如果旧 refresh token 又被使用,说明可能被盗,要废掉整个会话族。
详细版
JWT 校验清单(不只是验签):
| 校验项 | 说明 |
|---|---|
| 签名 | token 未被篡改(第一关,但不够) |
| 算法白名单 | 服务端固定允许的算法,不信 header 的 alg |
| 密钥(kid) | 多密钥场景按 kid 选,且密钥来源可信 |
iss | 发行方是可信的授权服务器 |
aud | 受众包含当前资源服务(token 是发给我的) |
exp | 未过期 |
nbf | 已生效(不晚于当前时间) |
| 业务声明/状态 | 权限、租户、用户是否被封禁(可能和 DB 不一致) |
Access Token vs Refresh Token:
| Access Token | Refresh Token | |
|---|---|---|
| 用途 | 频繁访问 API | 只用于换新 access token |
| 生命周期 | 短(分钟级) | 长(天级) |
| 泄露窗口 | 小 | 需可撤销、可检测 |
| 存放 | 内存/Authorization 头 | 服务端存哈希、HttpOnly Cookie/安全存储 |
完整版教学
一、签名只是第一关
很多人以为「JWT 验了签名就安全了」——远远不够。验签只能说明两件事:token 没被篡改、来自持有某个密钥的一方。但它不能说明这个 token 适合当前服务:
- 可能是别的系统用同一套签名机制签发的 token。
- 可能已经过期。
- 可能是发给另一个服务的 token(
aud不对)。
所以资源服务器验签之后,还必须校验发行方、受众、有效期、算法和业务声明,否则可能接受了不该接受的 token。签名是「防伪」,其他声明是「防误用」。
| 校验层次 | 典型字段 | 防住的问题 |
|---|---|---|
| 加密完整性 | 签名、算法白名单、kid | 篡改、算法混淆、错密钥 |
| 标准声明 | iss、aud、exp、nbf | 错发行方、错服务、过期或未生效 |
| 业务状态 | 权限、租户、用户状态、token version | 权限过期、封禁未生效、跨租户 |
JWT 校验要按“签名防伪、声明防误用、状态防过期权限”三层记,少一层都可能出安全洞。
二、算法和密钥选择(安全陷阱)
- 固定允许的算法(算法白名单):服务端应该写死自己接受的算法(如只接受 RS256),不能完全相信 token header 里的
alg。历史上有著名的alg=none攻击(攻击者把算法改成 none、去掉签名)和 RS256→HS256 混淆攻击(把非对称改成对称、用公钥当密钥伪造签名)。防护就是服务端固定算法,忽略 header 的 alg 主张。 - 多密钥用 kid:如果有多个签名密钥(轮换期间),token header 的
kid指明用哪个密钥验,但仍要确认这个密钥来源可信(从可信的 JWKS 端点获取)。 - 非对称签名的优势:RS256/ES256 等非对称算法——授权服务持私钥签发,多个资源服务只需公钥验证。公钥可以公开分发,私钥集中保管,适合多服务架构。
三、标准声明校验(时间与来源)
几个标准声明必须校验:
iss(issuer):发行方,要匹配你信任的授权服务器。aud(audience):受众,要包含当前资源服务的标识——确认「这个 token 是发给我这个服务用的」,防止拿着发给 A 服务的 token 去访问 B 服务。exp(expiration):不能过期。nbf(not before):不能晚于当前时间(token 还没生效不能用)。- 必要时校验
iat(签发时间)、jti(token 唯一 id,可用于防重放)。
时间校验可允许很小的时钟偏差(clock skew,如几十秒,容忍服务器间时钟不完全同步),但不能无限放宽。
now = 10:00:00, clock skew = 60s
nbf <= 10:01:00 才可接受
exp >= 09:59:00 才可接受
四、业务声明不能盲信(关键)
JWT 里常写入角色、权限、租户、用户状态等业务声明。但这些是「签发那一刻」的快照,在 token 过期前,可能和数据库最新状态不一致:
- 用户在 token 有效期内被封禁了,但他手里的 token 还没过期——如果只信 token 里的声明,他还能继续访问。
- 用户的权限被收回了,token 里的旧权限还在。
所以对高风险操作(转账、删除、改配置),不能只信 token 里的声明——应该实时查数据库确认用户状态、权限版本(或 token version)。这是 JWT「无状态」的固有代价:无状态换来了性能和扩展性,但牺牲了「实时撤销」的能力,高风险场景要用实时校验补回来。
五、Access Token 和 Refresh Token 的分工
双 token 机制是 JWT 认证的标准做法:
- Access Token:短命(几分钟~几十分钟),频繁用于访问 API。因为泄露窗口要尽量小——即使被偷,很快就过期。
- Refresh Token:长命(几天~几周),只用于换取新的 access token,不直接访问业务 API。它必须可撤销、可审计、可检测异常。
关键:不要把 refresh token 到处传给资源服务——它只在「刷新」这一个端点用,减少暴露面。access token 短命降低泄露风险,refresh token 通过可撤销来兜底。
六、轮换和复用检测(refresh token 安全核心)
Refresh Token 轮换(Rotation):每次用 refresh token 刷新时,签发一个新的 refresh token,并让旧的立即失效。
复用检测(Reuse Detection):因为旧的已失效,如果一个已失效的旧 refresh token 又被使用,这是一个强风险信号——说明它可能被窃取了(攻击者拿旧 token 来刷新),或者发生了异常复用。此时应该撤销整个「会话族」(token family)——把这条会话链上所有 token 全废掉,强制重新登录。
并发刷新问题:合法客户端也可能因为并发/重试同时用同一个 refresh token 刷新两次,这会被误判为复用。处理:用短暂宽限期或原子状态更新(数据库乐观锁)来区分「合法并发」和「真正的盗用」。
七、存储与客户端安全
- 服务端不要明文存 refresh token——至少存哈希(类似密码,泄露了也难还原)。
- 浏览器:token 放 HttpOnly + Secure + SameSite 的 Cookie(防 XSS 读取、防 CSRF);放 localStorage 容易被 XSS 偷(JS 能读)。
- 移动端:用系统的安全存储(Keychain/Keystore)。
- 所有 token 必须走 HTTPS(防中间人窃听)。
- 日志绝不打印完整 token(脱敏)。
八、常见误区与追问
- 误区:JWT 只要验签通过就安全。 验签只能证明未被篡改,还必须校验算法、发行方、受众、有效期和业务状态。
- 误区:可以完全相信 token header 里的
alg。 服务端必须使用算法白名单,避免alg=none或 RS/HS 混淆等历史攻击模式。 - 误区:Refresh Token 泄露风险比 Access Token 小。 Refresh Token 生命周期长且能换新 token,必须可撤销、轮换、复用检测。
- 追问:
aud校验解决什么问题? 防止把发给 A 服务的 token 拿去访问 B 服务,确认当前资源服务是 token 的受众。 - 追问:Refresh Token 复用检测发现旧 token 又被使用怎么办? 应视为疑似泄露,撤销整个 token family 并要求重新登录。
- 追问:用户被封禁后 JWT 还没过期怎么办? 高风险操作要查用户状态或 token version,不能只依赖 token 中的旧快照。
九、加强记忆
JWT 校验不只验签名——还要验:算法白名单(不信 header 的 alg,防 alg=none / RS256→HS256 攻击)、密钥(kid,来源可信,非对称让授权服务持私钥、资源服务用公钥)、iss(发行方)、aud(受众,token 是发给我的)、exp(未过期)、nbf(已生效)、业务声明/用户状态(可能和 DB 不一致,高风险操作要实时查)。Access Token 短命(频繁访问、泄露窗口小);Refresh Token 长命但可撤销(只用于刷新、别传给资源服务)。轮换 + 复用检测:每次刷新签发新的、旧的立即失效,旧 refresh token 再被用 = 疑似被盗 → 撤销整个会话族强制重登(并发刷新用宽限期/原子更新区分)。存储:服务端存哈希、浏览器用 HttpOnly Cookie(别放 localStorage)、走 HTTPS、日志不打完整 token。