现在的大部分 Web 系统、App、后台管理系统,认证方案几乎都绕不开 JWT(JSON Web Token)。
但很多项目一开始只会:
- 登录 → 发一个 Token
- 请求 → 校验 Token
随着系统复杂度增加,很快就会遇到:
- 用户退出后 Token 还能用
- 修改密码无法让旧 Token 失效
- 无法单设备踢人
- 无法强制下线
- Redis 压力过大
- 无法做到真正的"无状态"
于是 Token 机制会不断演进。
本文从简单到复杂,梳理一套完整的 Token 体系设计。
一、单 Token
最简单的方案:
用户登录成功后,服务器签发一个 JWT:
json
{
"user_id": 1001,
"exp": 1710000000
}
客户端之后每次请求:
http
Authorization: Bearer xxx
服务端:
- 校验 JWT 签名
- 校验 exp
- 解析 user_id
即可完成认证。
优点
1. 完全无状态
服务端不存储 Token。
天然适合:
- 微服务
- 分布式
- 多实例部署
2. 性能极高
只需要:
- JWT 解密
- HMAC/RSA 验签
无需查 Redis。
3. 实现极其简单
适合:
- 小项目
- Demo
- 内网系统
缺点
这是它最大的致命问题:
Token 一旦签发,在过期前永远有效。
即:
- 用户退出登录 → Token 还能继续用
- 修改密码 → 旧 Token 还能继续用
- 用户被封禁 → Token 还能继续用
- Token 泄露 → 无法立即失效
因此:
单 JWT 本质上无法"主动失效"。
二、单 Token + Redis 黑名单
为了解决 JWT 无法主动失效的问题。
开始引入:
Redis BlackList
退出登录时:
text
blacklist:token_xxx -> exp
请求时:
text
1. 校验 JWT
2. 查询 Redis 是否在黑名单
如果存在:
text
Token 已失效
优点
解决了:
- 登出立即失效
- 管理员强制下线
- Token 泄露后紧急封禁
缺点
1. Redis 压力增加
每次请求都需要:
text
Redis GET
高并发下压力明显。
2. Token 越多黑名单越大
尤其:
- 长时间 Token
- 海量用户
Redis 会不断增长。
3. 仍然只有一个 Token
用户频繁登录:
- Token 频繁过期
- 用户体验差
于是:
双 Token 体系开始出现。
三、双 Token(AT + RT)
现代 Web 系统最主流方案。
核心思想
拆成:
Access Token(AT)
短期有效:
text
15分钟
用于:
text
接口访问
Refresh Token(RT)
长期有效:
text
7天 / 30天
用于:
text
刷新 Access Token
登录流程
登录成功:
服务端签发:
text
AT(15分钟)
RT(7天)
请求流程
普通接口:
text
携带 AT
AT 过期:
text
使用 RT 换新 AT
优点
1. 安全性提升
即使:
text
AT 泄露
攻击窗口也只有:
text
15分钟
2. 用户体验好
用户无需频繁登录。
3. 适合移动端
App 可以长期在线。
缺点
仍然存在:
- 修改密码无法让旧 Token 失效
- 封禁用户无法立即失效
于是继续升级。
四、双 Token + token_version
这是很多中大型系统真正实用的方案。
核心思想
在数据库维护:
sql
token_version
用户表:
sql
users
├── id
├── password
├── token_version
签发 Token
AT:
json
{
"user_id": 1001,
"token_version": 3,
"exp": 1710000000
}
RT:
json
{
"user_id": 1001,
"token_version": 3,
"jti": "uuid",
"exp": 1710000000
}
请求时
服务端:
text
1. 校验 JWT
2. 查询用户 token_version
3. 对比 JWT 中的 token_version
不一致:
text
Token 已失效
为什么强大?
因为:
可以让某个用户的所有 Token 瞬间全部失效。
修改密码
sql
token_version++
效果:
- 所有设备掉线
- 所有 Token 作废
封禁用户
sql
token_version++
效果:
- 全平台强制下线
这是"用户级失效"
特点:
text
粒度:用户级
即:
text
让某用户所有 Token 全失效
缺点
1. 每次请求都需要查数据库
解决办法:
放 Redis:
text
user_token_version:1001 -> 3
2. 无法单设备踢下线
因为:
text
token_version 是用户级
只能:
text
全设备失效
五、双 Token + Redis 黑名单
这是很多项目开始"半状态化"的阶段。
为什么还要黑名单?
因为:
token_version 无法解决"单 Token 立即失效"。
比如:
用户主动退出登录:
你总不能:
sql
token_version++
否则:
text
所有设备全掉
所以:
退出登录时:
text
把当前 AT 加入 Redis 黑名单
适用场景
登出
text
AT 拉黑
单 Token 风控
比如:
text
检测到异常 IP
拉黑当前 Token。
缺点
仍然无法:
text
单设备 AT 即时踢出
因为:
AT 本身无状态。
六、双 Token + token_version + Redis 黑名单 + AT 映射表
这已经接近大型系统方案。
核心目标
实现:
单设备即时踢下线
核心设计
登录成功后:
text
user_at:1001:deviceA -> at_jti
user_at:1001:deviceB -> at_jti
或者:
text
at_mapping:{jti} -> user/device
请求时
除了:
- JWT 校验
- token_version 校验
- 黑名单校验
还需要:
text
校验当前 AT 是否仍然是最新映射
为什么需要它?
比如:
用户在新设备登录。
你希望:
text
旧设备立即下线
那么:
text
覆盖 Redis AT 映射
旧 Token 立刻失效。
这是"设备级失效"
粒度:
text
单设备
七、推荐方案
对于普通中大型项目。
我更推荐:
双 Token + token_version + RT 白名单
而不是:
text
AT 映射表
为什么?
因为:
AT 应该尽量无状态
AT 生命周期很短:
text
15分钟
没必要强行维护状态。
否则:
JWT 就退化成 Session 了。
推荐实践
AT(Access Token)
特点:
- 短期
- 无状态
- 不存 Redis
内容:
json
{
"user_id": 1001,
"token_version": 3,
"exp": 1710000000
}
有效期:
text
15分钟
RT(Refresh Token)
特点:
- 长期
- 有状态
- 存 Redis
Redis:
text
refresh_token:{jti}
内容:
json
{
"user_id": 1001,
"token_version": 3,
"jti": "uuid"
}
有效期:
text
7天
刷新流程
刷新时:
text
1. 校验 RT
2. 校验 Redis 中 jti
3. 删除旧 RT
4. 签发新 RT
5. 签发新 AT
这叫:
Refresh Token Rotation(RT轮换)
安全性远高于固定 RT。
八、各种方案对比
| 方案 | 能主动失效 | 单设备踢出 | 全设备失效 | Redis压力 | 安全性 |
|---|---|---|---|---|---|
| 单 Token | ❌ | ❌ | ❌ | 无 | 低 |
| 单 Token + 黑名单 | ✅ | ✅ | ✅ | 高 | 中 |
| 双 Token | 部分 | ❌ | ❌ | 低 | 中 |
| 双 Token + token_version | ✅ | ❌ | ✅ | 中 | 高 |
| 双 Token + 黑名单 | ✅ | 部分 | ✅ | 中 | 高 |
| 双 Token + AT映射 | ✅ | ✅ | ✅ | 高 | 很高 |
九、最终建议
小项目
推荐:
双 Token
简单够用。
中大型项目
推荐:
双 Token + token_version + RT轮换
这是性价比最高的方案。
强安全后台 / 金融系统
推荐:
双 Token + token_version + RT轮换 + 黑名单
必要时:
- 设备管理
- 单设备踢下线
- 风控系统
- IP 风险识别
十、核心认知
很多人误以为:
JWT = 无状态认证
实际上:
真正的大型系统几乎都是"半状态 JWT"
原因很简单:
你永远需要"主动失效能力"。
而:
text
完全无状态
与:
text
主动失效
天然冲突。
所以最终:
现代认证体系本质上都是:
"JWT + Redis状态控制"
只是:
状态维护到什么程度的问题。