借助Redis实现Token黑名单机制
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方法
/*** 将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方法
/*** 检查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方法
/*** 从黑名单中移除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方法
/*** 验证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加入黑名单
@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
@Component
public class TokenInterceptor implements HandlerInterceptor {@Autowiredprivate TokenUtil tokenUtil;@Overridepublic 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 管理员强制用户下线
@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操作的并发控制
