SpringBoot3+Jwt+Redis的Token有效性检查以及自动续期的实现

环境

springboot 3.1.5
maven:3.6.1
java:17

思路

  • 续期在后端完成,只给前端传递一个token,不采用两个token的方式,从而不增加前端工作量
  • 默认情况下解析Jwt token 时过期的token将抛出过期错误,因为需要自动续期,因此应忽略该错误。见代码片段1
  • token等信息保存在Redis中,并设置过期时间为token的过期时间。过期后保存信息自动删除 。修改此处过期时间实现延期
  • 记录每次前端请求的时间至Redis中。
  • 验证token有效性时,若距离最近一次请求的时差未超过定义的过期时间增量,则尝试进行续期操作。见代码片段2。代码注解很详细,就不在重复

代码片段1

  /**
     * 获取Claims
     *
     * @param bearerToken 带前缀的token
     * @return Claims
     */
    public static Claims getClaims(String bearerToken) {
        if (StringUtils.isBlank(bearerToken)) {
            return null;
        }
        Claims claims = null;
        try {
            String token = formBearerTokenToToken(bearerToken);
            if (StringUtils.isNotBlank(token)) {
                claims = Jwts.parser().setSigningKey(JwtParams.getSecret()).parseClaimsJws(token).getBody();
            }
        } catch (ExpiredJwtException e) {
            //过期依然返回claims
            claims = e.getClaims();
//            log.info("获取Claims时已过期, Error:{}; token:{}", e.getLocalizedMessage(), bearerToken);

        } catch (Exception e) {
            log.info("获取Claims时出错,token:{}; Error:{}", bearerToken, e.getLocalizedMessage());
        }
        return claims;
    }

代码片段2

 /**
     * @description 判断request的token是否有效,若过期尝试自动续期
     * <br>
     * <pre>
     *     判断及操作流程:
     *     1.从request中获取token,其是否格式正确(前缀及签发者)且含有userId。为假,直接返回false
     *     2.判断request中token是否与cache中的token一致。不一致,直接返回false
     *     3.request中的token,是否未过期
     *       3.1 token未过期,直接返回true!!
     *       3.2 token过期,判断最后一次操作与当前时间差是否超过配置的过期时间增量(JwtParams.getExpireTime())
     *         3.2.1 超过配置的过期时间增量,返回false
     *         3.2.2 未超过,尝试自动续期
     *           3.2.2.1 自动续期成功,返回true!!
     *           3.2.2.2 自动续期失败,返回false
     * <pre>
     * @param request    request
     * @return boolean 是否成功
     * @author MuYi
     * @date 2023/12/12 12:57
     */
    public boolean tokenIsValid(HttpServletRequest request) {
        if (request == null) return true;
        String bearerToken = JwtTokenUtils.getToken(request);
        // 验证request是否格式及签发者正确
        if (!JwtTokenUtils.tokenIsCorrect(bearerToken)) {
            return false;
        }
        Long userId = JwtTokenUtils.getUserId(bearerToken);
        // 验证令牌是否含userId
        if (userId == null) {
            log.info("token({})内无法解析有效用户id", bearerToken);
            return false;
        }
        //以上检查request中的token格式及签发者是否正确、否含userId
        //以下检查request与cache中的token是否一致
        String cacheToken = JwtCacheUtils.getTokenFromCache(userId);
        String requestInfo = request.getLocalAddr() + ":" + request.getLocalPort() + request.getRequestURI();
        //cache中无token。表示用户未登录或已注销或系统重启
        //依据前提:
        // 1 token的核验以cache中的token为准
        // 2 当用户登录时cache中保存其token
        // 3 当用户注销时、超时、系统关闭时删除其cache中的token
        if (cacheToken == null) {
            forceLogout(userId, "使用已被注销令牌", requestInfo);
            return false;
        }
        if (!bearerToken.equals(cacheToken)) {
            forceLogout(userId, "令牌不一致", requestInfo);
            return false;
        }
        // 已不再考察request中token的过期时间
        //        // 验证request中的token是否未过期
        //        if (!JwtTokenUtils.tokenIsExpired(bearerToken)) return true;

        //验证距离最后一次操作时间是否超过配置的过期时间增量
        long lastOperateTime = JwtCacheUtils.getLastOperateTime(userId).getTime();
        long currentTime = System.currentTimeMillis();
        //计算距离最后一次操作时间差,增加TOKEN_ADVANCE_CHECK_TIME时间量,以免出现误差
        long diff = currentTime + TOKEN_ADVANCE_CHECK_TIME - lastOperateTime;
        //时间差大于过期时间增量,表示令牌过期
        if (diff > JwtParams.getExpireDuration()) {
            forceLogout(userId, "令牌过期", requestInfo);
            return false;
        }
        //时间差小于配置的过期时间增量,表示令牌未过期。需计算局里上一次需求时差,避免频繁更新cache
        //尝试自动续期
        diff = currentTime - lastOperateTime;
        if (diff >= TOKEN_ADVANCE_CHECK_TIME) {
            // 在尝试自动续期前应用防抖逻辑
            String debounceKey = "renewalToken:" + userId;
            DebounceUtil.debounce(debounceKey, userId, (userIdToRenew) -> {
                Date renewalDate = JwtCacheUtils.renewalTokenExpireTimeout(userId);
                if (renewalDate == null) 
                    forceLogout(userId, "尝试自动续期失败", requestInfo);                
            }, ANTI_SHAKE_DELAY); // 假设设置防抖延迟为500毫秒
        }
        return true;
    }

    protected void forceLogout(Long userId, String msg, String requestInfo) {
        try {
            log.info("{},将强制用户(ID={})退出。(request:{})", msg, userId, requestInfo);
            logoutProcess(userId);
//                request.setAttribute("renewal",false);
        } catch (Exception e) {
            log.info("{},将强制用户(ID={})退出。(request:{})。发生错误{}", msg, userId, requestInfo, e.getLocalizedMessage());
        }
    }

BUG修复

  • 2024.6.4

    • tokenIsValid方法逻辑错误
      由于采用后台核对cache的token过期信息,且前端request中的token亦从未更新,因此不应再考察request中token的过期时间,否则整个方法逻辑都是错误的。所以,移除if (!JwtTokenUtils.tokenIsExpired(bearerToken)) return true;代码
    • 防抖函数应用
      由于tokenIsValid在某一个会被接连调用譬如间隔1毫秒。导致renewalTokenExpireTimeout(userId)方法可能在几毫秒内被多次调用,因此增加防抖函数。
      此方法非必须。
SpringBoot整合sa-token,可以按照以下步骤进行操作。 1. 添加依赖:在`pom.xml`文件添加sa-tokenRedis集成包依赖。可以使用官方提供的Redis集成包`sa-token-dao-redis-jackson`,具体依赖如下: ``` <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.34.0</version> </dependency> ``` 2. 配置sa-token:在SpringBoot的配置文件,配置sa-token的相关属性,包括Redis连接信息、token有效期等。可以参考sa-token的官方文档进行配置。 3. 注解鉴权:在需要进行鉴权的方法上添加相应的注解。例如,使用`@SaCheckLogin`注解表示该方法需要登录认证,使用`@SaCheckRole`注解表示该方法需要具有指定角色才能访问。可以根据具体需求选择合适的注解进行鉴权。 4. 注册拦截器:在高版本的SpringBoot(≥2.6.x),需要额外添加`@EnableWebMvc`注解才能使注册拦截器生效。可以在配置类上添加该注解。 通过以上步骤,就可以在SpringBoot成功整合sa-token,并实现基于注解的鉴权功能。请根据具体需求进行配置和使用。 #### 引用[.reference_title] - *1* [【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权](https://blog.csdn.net/weixin_43165220/article/details/126889045)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [springboot:整合sa-token](https://blog.csdn.net/weixin_43296313/article/details/124274443)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Spring Boot使用Sa-Token实现轻量级登录与鉴权](https://blog.csdn.net/m0_71777195/article/details/129175616)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

muyi517

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值