1. 写在前面
众所周知,token作为请求时验证用户信息的令牌,它是无状态的。
所以当用户退出登录时,前端将localStorage清空掉,然后再跳转到登录页就行了。
那么要不要再向服务端发送请求呢,这个请求要做什么呢?
请求肯定是要的,就算用户已经退出登录了,这个时候token可能还是没有过期的。
如果有人拿着这个token伪造一下身份向服务器发一些请求,这不就危险了。
所以,本文以一种手动失效token的方式来解决这个问题。
2. 怎么做呢
这是一个token工具类,有一个生成token和校验token的方法,具体实现就省略掉了,可参照下面这篇文章。
/**
* @author denchouka
* @description Token工具类
* @date 2024/10/26 21:07
*/
public class TokenUtils {
/**
* 生成token
* @return 生成的token
*/
public static String token() {
// 省略
}
/**
* 校验token有效性
* @param token 请求头中携带的token
* @return 校验不通过
*/
public static boolean verify(String token) {
// 省略
}
}
然后呢,再写一个方法revokeToken,在退出登录时调用,来手动失效token。
做法就是创建一个token的黑名单,把要手动失效的token加进去。
具体实现如下。
/**
* 过期token的黑名单
*/
private static final ConcurrentHashMap<String, Long> blackList = new ConcurrentHashMap<>();
/**
* 退出登录的时候手动失效token
* @param token 要失效的token
*/
public static void revokeToken(String token) {
// 如果校验不通过直接返回
if (!verify(token)) {
return;
}
DecodedJWT decode = JWT.decode(token);
// 获取token的jti
String jti = decode.getId();
// 获取token的失效时间
Long exp = decode.getClaim(RegisteredClaims.EXPIRES_AT).asLong();
// 加入到黑名单
blackList.put(jti, exp);
}
简单说明一下,
创建了一个成员变量blackList(黑名单)用来存要手动失效的token,考虑到线程安全就用ConcurrentHashMap,key是token的jti,value是这个token的失效时间。
获取token的jti,jti是JWT的唯一的id。用jti做黑名单的key是因为token太长了,而jti由具有唯一性所以就选它了。补充一点,jti是在生成token时自己指定的,所以如果要使用jti需要保证是唯一的。
exp就是token的失效时间,在生成token时就已经有了。
加入到黑名单,这样就算是手动失效了。
如果刚好在token过期时间的临界点,在走到这个方法的时候已经过期了。所以在方法一开始调一下校验token的方法,如果校验不通过后边就直接返回。
加入黑名单之后,还需要修改校验token的方法,在每次校验token时先判断一下如果token在黑名单里,就直接返回校验不通过。这么做就是为了防止一些伪造的请求。
/**
* 校验token有效性
* @param token 请求头中携带的token
* @return 校验结果
*/
public static boolean verify(String token) {
// 省略
// 判断是否被手动失效过
if (isTokenExpired(token)) {
return false;
}
// 省略
}
/**
* 校验token时判断token是否失效
* @param token 要判断的token
*/
private static boolean isTokenExpired(String token) {
DecodedJWT decode = JWT.decode(token);
// 获取token的jti
String jti = decode.getId();
// 只要在黑名单里就是手动失效的
return blackList.containsKey(jti);
}
到这里呢就还有个问题,时间长了这个黑名单会越来越大,得定时清理一下。
定时任务每一个小时执行一次,是因为我在生成token时指定的过期时间就是一个小时(可灵活)。
只要黑名单里token的过期时间小于等于当前系统时间,就说明已经过了过期时间了,直接删除掉。
/**
* 定时清理手动失效的token
*/
@Scheduled(cron = "0 0 * * * ?")
public void cleanRevokeToken() {
// 清理已经失效的
long now = Instant.now().getEpochSecond();
blackList.entrySet().removeIf(entry -> entry.getValue() <= now);
}
当然也可以选择把失效token存到mysql或redis中,思路是差不多的,可自主选择。
打完收工。