Tch
Tch
Published on 2024-12-22 / 6 Visits
0
0

如何安全地处理用户退出登录后的JWT Token

1. 写在前面

众所周知,token作为请求时验证用户信息的令牌,它是无状态的。

所以当用户退出登录时,前端将localStorage清空掉,然后再跳转到登录页就行了。

那么要不要再向服务端发送请求呢,这个请求要做什么呢?

请求肯定是要的,就算用户已经退出登录了,这个时候token可能还是没有过期的。

如果有人拿着这个token伪造一下身份向服务器发一些请求,这不就危险了。

所以,本文以一种手动失效token的方式来解决这个问题。

2. 怎么做呢

这是一个token工具类,有一个生成token和校验token的方法,具体实现就省略掉了,可参照下面这篇文章。

https://tch.cool/archives/lSrBv05Q
/**
 * @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);
}

简单说明一下,

  1. 创建了一个成员变量blackList(黑名单)用来存要手动失效的token,考虑到线程安全就用ConcurrentHashMap,key是token的jti,value是这个token的失效时间。

  2. 获取token的jti,jti是JWT的唯一的id。用jti做黑名单的key是因为token太长了,而jti由具有唯一性所以就选它了。补充一点,jti是在生成token时自己指定的,所以如果要使用jti需要保证是唯一的。

  3. exp就是token的失效时间,在生成token时就已经有了。

  4. 加入到黑名单,这样就算是手动失效了。

  5. 如果刚好在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中,思路是差不多的,可自主选择。

打完收工。


Comment