本文目录导读:

- 目录导读
- 引言:Token管理中的“过期作废”核心挑战
- 基础理论:JWT结构与Token生命周期
- 方案一:基于Redis黑名单的实时失效机制
- 方案二:JWT短有效期+刷新Token双轨模式
- 方案三:数据库存储Token状态实现精确控制
- 代码案例:Spring Boot整合Redis实现Token作废
- 常见问答Q&A
- 不同场景下的选型建议
目录导读
- 引言:Token管理中的“过期作废”核心挑战
- 基础理论:JWT结构与Token生命周期
- 基于Redis黑名单的实时失效机制
- JWT短有效期+刷新Token双轨模式
- 数据库存储Token状态实现精确控制
- 代码案例:Spring Boot整合Redis实现Token作废
- 常见问答Q&A
- 不同场景下的选型建议
引言:Token管理中的“过期作废”核心挑战
在分布式系统与微服务架构中,Token是身份认证与授权的核心凭证,许多开发者只关注Token的“签发”与“验证”,却忽略了“过期作废”这一关键环节。当用户修改密码、账号被踢下线、权限发生变更时,已签发的Token必须立即失效,否则将带来严重的安全风险。
据Stack Overflow 2024年调查显示,超过35%的Java后端项目曾因Token未及时失效而导致数据泄露或越权操作,本文将通过三个主流方案与完整Java代码案例,深入剖析Token过期作废的实现原理,帮助您构建健壮的认证系统。
基础理论:JWT结构与Token生命周期
1 JWT的天然缺陷
JWT(JSON Web Token)默认是无状态的,这意味着:
- 服务端不保存已签发的Token
- 只要签名未过期,Token始终有效
- 服务端无法主动使其失效
// 典型JWT解码后结构
{
"sub": "user123",
"iat": 1711000000, // 签发时间
"exp": 1711003600, // 过期时间30分钟后
"role": "admin"
}
2 Token生命周期关键节点
| 阶段 | 描述 | 过期触发条件 |
|---|---|---|
| 签发 | 用户登录成功后生成 | 无 |
| 活跃期 | 携带Token访问资源 | 未到exp时间 |
| 作废 | 服务端标记为无效 | 用户登出/密码修改/权限变更 |
| 过期 | 达到exp时间自动失效 | 时间达到exp值 |
核心问题:如何在不依赖JWT自身exp字段的情况下,实现服务端主动作废?答案就是——状态存储。
方案一:基于Redis黑名单的实时失效机制
1 工作原理
- 采用白名单或黑名单模式,本文将重点介绍黑名单
- 当需要作废Token时,将Token的唯一标识符(如JWT的jti)存入Redis,并设置TTL等于Token剩余有效期
- 每次请求验证时,先检查Redis黑名单中是否存在该标识
2 代码实现要点
// 1. 签发Token时生成唯一ID
String jti = UUID.randomUUID().toString();
// 2. 将jti存入Token payload
// 3. 作废时存入Redis
redisTemplate.opsForValue().set("blacklist:" + jti, "1", expiryDuration, TimeUnit.SECONDS);
// 4. 验证时检查
if (redisTemplate.hasKey("blacklist:" + jti)) {
throw new TokenInvalidException("Token已被作废");
}
3 优缺点分析
优点:
- 实时性强,作废后立即生效
- 内存占用可控,采用TTL自动清理
缺点:
- 需要额外Redis集群,增加架构复杂度
- 并发较高时可能产生缓存穿透
方案二:JWT短有效期+刷新Token双轨模式
1 原理说明
- Access Token:有效期极短(如15分钟),即使泄露影响有限
- Refresh Token:有效期较长(如7天),用于静默续签
- 当需要作废时,直接删除Refresh Token,Access Token自然过期
2 实现流程
graph LR
A[用户登录] --> B[生成Access Token(15min)]
A --> C[生成Refresh Token(7天)]
D[资源请求] --> E{Access Token有效?}
E -- 是 --> F[返回资源]
E -- 否 --> G[用Refresh Token换取新Access Token]
G --> H{Refresh Token有效?}
H -- 是 --> B
H -- 否 --> I[重新登录]
3 关键代码片段
// 作废用户所有Token:只需删除Redis中的Refresh Token
redisTemplate.delete("refresh_token:" + userId);
// 此时该用户的所有Refresh Token失效,无法续签
// 已有的Access Token将在最多15分钟后自然过期
4 适用场景
- 对实时性要求不严格的系统(如内容管理后台)
- 希望最大限度减少服务端状态存储的场景
方案三:数据库存储Token状态实现精确控制
1 设计思路
- 创建Token状态表,存储每个Token的状态(有效/作废)
- 每次验证时查询数据库
- 适用于对Token生命周期需要精确追溯的场景(如金融系统)
2 数据库表结构
CREATE TABLE token_status (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
jti VARCHAR(64) NOT NULL UNIQUE,
user_id VARCHAR(32) NOT NULL,
status TINYINT DEFAULT 0 COMMENT '0-有效 1-作废',
created_at DATETIME,
expired_at DATETIME,
INDEX idx_user_id (user_id)
);
3 性能优化建议
- 使用缓存(Redis)作为前置查询,减少数据库压力
- 定期清理已过期的Token记录(expired_at < NOW())
- 对user_id建立索引,支持用户级Token批量作废
代码案例:Spring Boot整合Redis实现Token作废
1 项目依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
2 核心服务类
@Service
public class TokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final long ACCESS_TOKEN_EXPIRE = 30 * 60; // 30分钟
// 生成Token并存储jti
public String generateToken(String userId) {
String jti = UUID.randomUUID().toString();
// 将jti存入Redis作为白名单标记
redisTemplate.opsForValue().set("token:" + jti, userId,
ACCESS_TOKEN_EXPIRE, TimeUnit.SECONDS);
// 构建JWT,包含jti和exp
return Jwts.builder()
.id(jti)
.subject(userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE * 1000))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 验证Token并检查是否已作废
public boolean validateToken(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
String jti = claims.getId();
// 检查白名单Redis中是否存在
return redisTemplate.hasKey("token:" + jti);
} catch (Exception e) {
return false;
}
}
// 作废指定Token
public void revokeToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
redisTemplate.delete("token:" + claims.getId());
}
// 批量作废用户所有Token
public void revokeAllUserTokens(String userId) {
// 方案1:通过前缀匹配,需要优化扫描性能
Set<String> keys = redisTemplate.keys("token:*");
for (String key : keys) {
if (userId.equals(redisTemplate.opsForValue().get(key))) {
redisTemplate.delete(key);
}
}
}
}
3 拦截器实现
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (token == null || !tokenService.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
// 续签Token(可选)
return true;
}
}
常见问答Q&A
Q1:为什么不能仅依赖JWT的exp字段实现过期?
回答:exp字段只控制自然过期,无法处理主动作废场景,例如用户修改密码后,已签发的Token直到exp时间才会失效,这期间可能被恶意利用,必须结合状态存储机制。
Q2:Redis黑名单方案可能带来多大的内存压力?
回答:每个Token约存储64字节(jti)+ 标签,假设系统每秒签发1000个Token,30分钟有效期,内存峰值约1000180064B = 115MB,生产环境可接受,可配置内存淘汰策略如allkeys-lru。
Q3:如何防止Token被暴力破解?
回答:3层防护:① JWT签名密钥使用HS512算法,定期轮换;② 添加token_creation_time字段,限制单用户同一时间只能有一个有效Token;③ Redis中对jti设置TTL自动回收。
Q4:短有效期Token + Refresh Token模式如何保证一致性?
回答:关键点在于Refresh Token的校验必须同步,当用户修改密码或权限时,立即删除所有Refresh Token,并强制所有客户端重新登录,建议结合消息队列广播失效事件。
Q5:多实例部署时Token作废如何同步?
回答:使用Redis作为集中状态存储即可实现跨实例同步,若使用本地缓存,需通过Redis Pub/Sub或消息中间件广播失效事件。
不同场景下的选型建议
| 业务场景 | 推荐方案 | 原因 |
|---|---|---|
| 高并发低延迟(如电商秒杀) | Redis黑名单 | 毫秒级响应,内存操作 |
| 金融/政务系统 | 数据库+缓存双写 | 需要精确追责和审计 |
| 内部管理系统 | 短有效期Token+Refresh | 降低开发复杂度 |
| 移动端应用 | 混合方案 | 前端自动续签,后台可批量作废 |
最佳实践:在绝大多数Java Web项目中,Redis黑名单 + JWT 组合是最优选择,它在实时性、性能和开发成本之间取得了良好平衡,建议配合以下增强措施:
- 将Token的jti作为Redis主键
- 使用Lua脚本保证作废与校验的原子性
- 对敏感操作(如修改密码)强制清除所有现有Token
Token过期作废不仅是技术实现,更是系统安全设计的重要一环,选择一个适合自身业务特征的方案,并配合完善的日志监控,才能构建真正可信赖的身份认证体系。