先记住这个答案
JWT 的无状态指服务端不保存会话,但撤销能力可通过附加状态实现。常用方案包括:维护令牌黑名单,在登出或改密时把 jti 加入黑名单并校验;缩短 Access Token 有效期并配合 Refresh Token 轮换,容忍短暂失效窗口;或在令牌中携带版本号,用户改密时递增版本并校验。黑名单最直接但有存储与查询开销,短有效期牺牲实时性,版本号需要集中式状态。实际系统常组合使用:短时 Access Token 加黑名单覆盖敏感操作。
- 黑白名单是集中状态,打破无状态
- 短有效期通常须配合刷新令牌使用
- 版本号适合全局踢出所有旧令牌
撤销的核心机制与代价
JWT的无状态依赖签名验证,服务端不查数据库。撤销的本质是让已签发的令牌在验证时被拒绝。黑名单方案将注销的jti存入Redis等存储,验证时先查黑名单,命中则拒绝。这保留了JWT的载荷自包含优点,但每次请求增加一次读操作,且黑名单需要设置过期时间(与token剩余有效期一致),否则条目将永久存在,使存储持续增长。
短有效期方案把 Access Token 设为 15 分钟,另发长期 Refresh Token。登出时只删除 Refresh Token,Access Token 在剩余几分钟内仍可用,形成可接受的失效窗口。若要求毫秒级失效,则需叠加黑名单。令牌版本号方案在用户表中存 version,JWT 中放入 ver 声明,改密时 version+1,验证时比对版本,不一致即拒绝。此方案天然支持踢出所有旧设备,但每次验证需访问用户存储(或缓存),变成有状态。
用户改密后强制所有旧令牌失效
假设某金融系统要求用户修改密码后,所有已登录设备必须在 1 秒内被登出,旧 token 不能继续访问接口。技术栈为 Spring Boot + Redis。若只用短有效期,Access Token 是 30 分钟,明显不满足要求;若用黑名单,需把所有旧 token 的 jti 加入 Redis,但用户可能不知道有哪些 token,只有服务端发过,追踪困难。
采用令牌版本号方案:用户表中增加 token_version 字段,登录签发的 JWT 中写入 ver。修改密码时执行 UPDATE users SET token_version = token_version + 1 WHERE id = ?,同时删除该用户的 Refresh Token 记录。每次请求鉴权时,从 JWT 解析出 user_id 和 ver,再用 Redis 缓存用户版本(键为 user:version:{id}),若 Redis miss 则查库并回填。若 ver 与存储值不符则拒绝。此方案将撤销从 O(n) 降为 O(1),实时性最好。代价是每次请求或至少每几分钟要检查一次版本,可通过本地缓存加短 TTL 减轻压力。
失败条件与选择边界
黑名单方案的失效应答是:如果忘记给黑名单条目设置 TTL,Redis 内存会持续增长;若 TTL 设置过短,在 token 有效期内条目提前消失,导致撤销失效。因此黑名单 TTL 必须等于 JWT 剩余有效期。此外,分布式环境下多个实例需要共享黑名单存储,否则出现某个实例仍放行。
短有效期方案的边界是撤销不彻底:Access Token 在有效期内无法收回,必须接受潜在泄露窗口。对于高安全场景不可单独使用。版本号方案要求每次验证都必须拉到最新版本,如果使用缓存,缓存更新延迟会造成撤销延迟;若把版本号放入 JWT,但验证时又去查库,实际等于有状态了。另外,版本号无法只撤销某一个设备,它只能全量撤销所有令牌。若需细粒度控制单个 token,应使用 jti 黑名单。
容易答错的地方
- 认为 JWT 完全不能撤销
- 实际上 JWT 无状态但可通过服务端引入状态实现撤销,黑名单、版本号等方案都能让已发放的 token 失效,只是增加了存储或查询成本。误解源于将无状态等同于无法控制。
- 登出只删除本地 token
- 前端删除 token 只影响本地,已发出的 token 在服务器仍有效。攻击者若已复制 token,删除本地毫无用处。正确做法是服务端做黑名单或版本失效,并让 Access Token 短命。
面试官还会怎么问?
黑名单中的 jti 应该怎么生成?
jti必须是唯一标识,通常用UUID或随机数。撤销时需将jti加入黑名单存储;若为了在撤销时能枚举某用户的所有令牌,可在签发时将其与用户关联记录在单独的存储中,但这并非黑名单本身。也可直接用JWT的哈希作为标识,但注意哈希碰撞。简单做法是使用UUID v4。
Refresh Token 如何撤销?
Refresh Token 通常存储在服务端数据库或 Redis 中,登出或改密时直接删除对应记录即可。若采用轮换机制,旧 Refresh Token 在下次刷新时会被检测到重用并撤销整个会话。
微服务下撤销状态如何共享?
黑名单或版本号都存在集中式存储(Redis),所有服务实例通过同一 Redis 校验。若追求高可用,Redis 需集群部署。另一种方式是用网关统一校验,但内部服务间调用仍需信任,最好也校验。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。