公司动态
JWT 为什么不能真正退出登录?——从无状态认证到双token
深入理解 JWT 无法真正退出登录的原因并结合 Session、双 Token、Redis 等方案分析现代认证系统的设计思路。很多人都会使用 JWT 做登录认证但真正理解JWT 为什么无法立即退出登录的人并不多。本文不会介绍 JWT 的基础知识而是围绕一个问题展开为什么 JWT 不能像 Session 一样实现真正意义上的退出登录一、为什么会有这个问题一个典型的 JWT 登录流程如下用户名 密码 │ ▼ 生成 JWT │ ▼ 前端保存 Token │ ▼ 以后所有请求都携带 Token整个过程看起来没有任何问题。但是当用户点击退出登录时真正发生了什么很多人第一反应都是删除 Token。那么问题来了。JWT 保存在客户端服务器并没有保存它的登录状态。服务器无法主动删除用户浏览器中的 Token也无法通知已经签发的 Token 立即失效。因此一个核心问题出现了JWT 到底是如何实现退出登录的二、先看看 Session 是怎么退出登录的理解 JWT 之前先看看传统 Session 的工作方式。登录流程如下浏览器 │ 登录 │ ▼ SessionID │ ▼ 服务器 Session 表服务器保存着用户的登录状态例如SessionID │ ├── 用户ID ├── 登录时间 ├── 权限信息 └── ...当用户退出登录时删除 Session之后浏览器再次请求浏览器发送 SessionID │ ▼ 服务器查询 Session │ ▼ 不存在 │ ▼ 登录失效因为登录状态保存在服务器。服务器拥有登录状态的控制权因此可以随时让用户下线。三、JWT 为什么不能这样JWT 与 Session 最大的区别就是JWT 是无状态Stateless的。一个典型的 JWT 登录流程浏览器 │ JWT │ ▼ 服务器验证签名 │ ▼ 通过JWT 本身由三部分组成Header Payload Signature例如 Payload{uid:1001,role:admin,exp:1750000000}服务器收到 JWT 后一般只会进行以下几步解析 Token │ ▼ 验证签名 │ ▼ 检查 exp 是否过期 │ ▼ 认证成功在纯 JWT 实现中这个过程通常不查询数据库不查询 Redis不查询 Session也就是说服务器不会保存 JWT 的登录状态。因此它无法主动撤销一个已经签发且仍未过期的 Token。四、为什么服务器删不了 JWTJWT 通常保存在客户端例如localStorageCookieMemory内存服务器没有权限删除浏览器中的数据。因此所谓的退出登录浏览器删除 Token其实只是客户端主动配合删除 Token。如果有人提前复制了这个 TokenAuthorization: Bearer xxxxxxxxx即使用户已经点击退出登录浏览器删除 Token攻击者仍然可以继续发送Bearer xxxxxxxxx服务器验证签名正确Token 未过期依然会认证成功。直到exp 到期例如15 分钟Token 才真正失效。五、为什么 AccessToken 要设置较短有效期既然 JWT 无法立即失效那么风险如何控制最直接的方法就是缩短 AccessToken 的有效期。例如AccessToken 15 分钟即使 Token 泄露最多 15 分钟后自动失效因此实际项目通常推荐530 分钟而不是7 天 30 天 90 天六、但是频繁登录用户体验太差如果 AccessToken 只有15 分钟用户正在写一篇博客。突然401 Unauthorized Token 已过期要求重新登录。显然体验非常差。于是双 TokenDouble Token出现了。七、双 Token 是如何工作的登录成功后用户名密码 │ ▼ 登录成功 │ ├──────────────┐ ▼ ▼ AccessToken RefreshToken 15 min 7 day之后所有业务接口AccessToken只有刷新接口RefreshToken整个流程AccessToken 过期 │ ▼ 返回 401 │ ▼ 前端携带 RefreshToken 请求刷新接口 │ ▼ 服务器签发新的 AccessToken │ ▼ 继续业务请求这样AccessToken 可以设置得很短用户却可以持续登录数天兼顾了安全性和用户体验。八、为什么 RefreshToken 要存 Redis很多人都会疑惑JWT 不是无状态吗为什么 RefreshToken 又放进 Redis原因其实很简单RefreshToken 需要可控。例如Redis refresh:1001 ↓ eyJhbGc...刷新流程收到 RefreshToken │ ▼ 查询 Redis │ ▼ 一致 │ ▼ 签发新的 AccessToken退出登录时删除 Redis 中的 RefreshToken │ ▼ RefreshToken 立即失效这样服务器重新获得了登录状态的控制权。九、为什么 AccessToken 不放 Redis很多新人都会想到那 AccessToken 也放 Redis不就能立即失效了吗当然可以。但是 JWT 最大的优势就是绝大多数请求只需要验证签名即可完成认证。如果每次请求都要收到请求 │ ▼ 验证 JWT │ ▼ 查询 Redis │ ▼ 判断是否有效那么JWT 就失去了无状态认证带来的性能优势。因此大多数项目采用AccessToken ↓ 只验签 ↓ 不查 Redis而RefreshToken ↓ 查 Redis这是目前互联网最常见的设计方案。十、为什么退出登录后还能继续访问假设AccessToken15 分钟 RefreshToken7 天用户退出登录删除 Redis 中 RefreshToken此时RefreshToken 已经失效。但是AccessToken 仍然签名合法尚未过期因此15 分钟内 仍然可以访问接口很多人第一次发现这个现象都会怀疑我的退出登录是不是写错了其实并不是。这是 JWT 无状态认证的天然特性。十一、如果要求立即下线怎么办如果业务要求用户退出后立即失效。通常有以下几种方案。方案一Token 黑名单Redis │ blacklist │ Token请求流程JWT 验签 │ ▼ 查询黑名单 │ ├── 存在 → 拒绝 └── 不存在 → 放行优点可以立即失效缺点每次请求都需要查询 Redis方案二Token Version数据库User token_version 5JWT 中token_version 5修改密码token_version之后所有旧 Token全部失效很多大型系统都会采用这种方案。方案三单设备登录Redislogin:1001 ↓ 最新 RefreshToken新设备登录覆盖 Redis旧设备刷新 Token失败最终自然退出登录。总结JWT 最大的特点不是不需要登录。而是无状态Stateless。无状态意味着服务器默认不会保存 JWT 的登录状态因此无法像 Session 一样主动撤销一个已经签发且未过期的 Token。AccessToken │ 短期有效 │ 只验签 │ 无需查询 Redis RefreshToken │ 长期有效 │ 存 Redis │ 可撤销这种方案兼顾了安全性用户体验服务端控制能力因此也成为了绝大多数互联网项目的标准实践。最后用一句话总结本文JWT 本身无法真正实现立即退出登录双 Token Redis 的核心目的并不是让 JWT 更安全而是让服务器重新获得对登录状态的控制权在安全性、性能和用户体验之间取得平衡。