1. 概述
如果没有为Token提供主动失效机制。一旦Token被签发,在过期之前将一直有效,存在以下安全隐患:
- 无法主动注销:用户注销后,已签发的Token仍然有效
- 安全风险:Token泄露后无法立即失效
- 缺乏控制:无法对特定Token进行精准控制
本博客通过将已失效的Token存储在Redis中,可以确保Token在被主动注销后无法继续使用,提高了系统的安全性和可控性。
2. 具体方案
2.1 设计思路
通过在Redis中维护一个Token黑名单来实现Token的主动失效功能:
- 当用户注销或Token需要主动失效时,将Token加入黑名单
- 在验证Token时,先检查是否在黑名单中
- 如果在黑名单中,则认为Token已失效
2.2 Redis存储设计
Key: token:blacklist:{token}
Value: "blacklisted"
Expire: Token剩余有效时间
2.3 TokenUtil中新增方法
2.3.1 addToBlacklist方法
java
/**
* 将Token加入黑名单,实现主动失效
*
* @param token JWT token
* @return 是否成功加入黑名单
*/
public boolean addToBlacklist(String token) {
try {
// 获取Token的过期时间
Date expirationDate = getExpirationDateFromToken(token);
if (expirationDate == null) {
return false;
}
// 计算剩余有效时间
long remainingTime = expirationDate.getTime() - System.currentTimeMillis();
if (remainingTime <= 0) {
return false;
}
// 将Token加入Redis黑名单,设置过期时间为Token剩余有效时间
String key = TOKEN_BLACKLIST_PREFIX + token;
return redisUtil.set(key, "blacklisted", remainingTime / 1000);
} catch (Exception e) {
return false;
}
}
2.3.2 isTokenInBlacklist方法
java
/**
* 检查Token是否在黑名单中
*
* @param token JWT token
* @return 是否在黑名单中
*/
public boolean isTokenInBlacklist(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
return redisUtil.exists(key);
}
2.3.3 removeFromBlacklist方法
java
/**
* 从黑名单中移除Token
*
* @param token JWT token
* @return 是否成功移除
*/
public boolean removeFromBlacklist(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
return redisUtil.del(key) > 0;
}
2.3.4 修改validateToken方法
java
/**
* 验证Token是否合法且未过期
*
* @param token JWT token
* * @return 是否有效
*/
public Boolean validateToken(String token) {
try {
// 检查Token是否在黑名单中
if (isTokenInBlacklist(token)) {
return false;
}
JwtParser parser = Jwts.parser().verifyWith(secretKey).build();
parser.parseSignedClaims(token);
return true;
} catch (JwtException e) {
System.out.println("JWT exception: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("JWT claims string is empty: " + e.getMessage());
}
return false;
}
3. 使用示例
3.1 用户注销时将Token加入黑名单
java
@PostMapping("/logout")
public ResultBean<String> logout(@RequestHeader("Authorization") String token) {
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
// 将Token加入黑名单
boolean added = tokenUtil.addToBlacklist(token);
if (added) {
return ResultBean.success("注销成功");
}
}
return ResultBean.error("注销失败");
}
3.2 在拦截器中验证Token
java
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private TokenUtil tokenUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
// 验证Token(会自动检查黑名单)
if (!tokenUtil.validateToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
}
return true;
}
}
3.3 管理员强制用户下线
java
@PostMapping("/admin/forceLogout")
public ResultBean<String> forceLogout(@RequestParam String token) {
// 将Token加入黑名单
boolean added = tokenUtil.addToBlacklist(token);
if (added) {
return ResultBean.success("用户已强制下线");
}
return ResultBean.error("操作失败");
}
4. 优化效果
4.1 安全性提升
- 实现了Token的主动失效功能
- 可以精准控制特定Token的可用性
- 有效防止Token泄露后的安全风险
4.2 控制性增强
- 提供了细粒度的Token控制能力
- 支持用户主动注销和管理员强制下线
- 可以根据业务需求灵活控制Token生命周期
4.3 用户体验改善
- 用户注销后Token立即失效
- 管理员可以强制用户下线
- 提高了系统的安全性和可控性
5. 注意事项
- Redis性能: 黑名单存储在Redis中,需要注意Redis的性能和容量
- 过期时间: 黑名单中的Token会自动过期,过期时间与Token剩余有效时间一致
- 异常处理: 需要妥善处理Redis操作可能出现的异常
- 并发控制: 在高并发场景下需要注意Redis操作的并发控制