【shiro】shiro整合JWT——4.JWT Token刷新/续签

前言

之前在写shiro整合JWT的时候,在ShiroRealm中有写到token的刷新;但后来看了很多别人的项目demo和博客发现之前的写法不太合适。这里参考之前看过的各个项目与博客,延续这之前shiro整合JWT内容的做了一波缝合怪。
主要对之前的ShiroRealm,JwtUtil,JwtFilter类进行修改。
ps:本文主要以记录核心思路为主。

1、Token设计

1.1 Token的情况

声明:Token设计内容参考JWT Token刷新方案

1、正常Token:Token未过期,且未达到建议更换时间。
2、濒死Token:Token未过期,已达到建议更换时间。
3、正常过期Token:Token已过期,但存在于缓存中。
4、非正常过期Token:Token已过期,不存在于缓存中。

1. 正常Token传入
当正常Token请求时,返回当前Token。
2. 濒死Token传入
当濒死Token请求时,获取一个正常Token并返回。
3. 正常过期Token
当正常过期Token请求时,获取一个正常Token并返回。
4. 非正常过期过期Token
当非正常过期Token请求时,返回错误信息,需重新登录。

1.2 具体设计

根据Token的情况,我们设置 token有效时间 外,还需要在设置一个 token刷新时间(建议更换时间)token缓存有效期(redis缓存)

  • 判断是否需要刷新
    token有效时间 - 当前时间 <= token刷新时间
    true:需要刷新
    false:有效期内
    在这里插入图片描述

2、代码实现

2.1 Maven依赖

        <!--JWT-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

2.2 JwtUtil

根据1.2具体设计的内容,修改JwtUtil。

2.2.1 新变量与checkRefresh方法

	/** token cache过期时间30分钟 设置redis过期时间**/
    public static final long CACHE_EXPIRE_TIME = 30 * 60 * 1000L;
    /** token 过期时间15分钟 **/
    public static final long EXPIRE_TIME = 15 * 60 * 1000L;
    /** token 最后5分钟更新token **/
    public static final long REFRESH_TIME = 5 * 60 * 1000L;

	/**
     * 判断是否需要刷新
     * @param cacheToken 缓存中的token
     * @param currentTime 当前时间
     * @return
     */
    public static boolean checkRefresh(String cacheToken, long currentTime){
    
    
        // 获取token的生成时间
        long current= (long) JwtUtil.getExpire(cacheToken);
        // token有效时间-当前时间↑ <= 需要刷新的有效时间?true需要刷新:false有效期内
        if (current+JwtUtil.EXPIRE_TIME - currentTime <= JwtUtil.REFRESH_TIME)
            return true;
        else
            return false;
    }

redis缓存token的时间一般为token过期时间的2倍。这里设置了3个静态变量是为了更直观的阅读理解各个地方表示的时间;在实际开发中,只需要设置EXPIRE_TIME就可以,而CACHE_EXPIRE_TIME直接替换为2 * EXPIRE_TIMEREFRESH_TIME使用到的地方只有刷新判断,所以不需要单独设置一个静态变量。

2.2.2 修改verify方法

    /**
     * 校验token是否被篡改、伪造或过期
     * 校验token的有效性,1、token的header和payload是否没改过;2、没有过期
     * @param token
     * @return
     */
    public static String verify(String token) {
    
    
        try {
    
    
            // 根据密钥(这里是密码)生成一个算法实例
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            // 生成JWT效验器
            JWTVerifier verifier = JWT.require(algorithm) // 设置一个以该算法为基础的校验器
                                      .acceptLeeway(2) // 设置允许误差时间
                                      .build(); // 创建校验器
            // 效验TOKEN,验证JWT是否有效,包括过期时间的判断,篡改、伪造或过期就会出现异常
            DecodedJWT jwt = verifier.verify(token);
            return "success";
        } catch (SignatureVerificationException e){
    
    
            // 签名无效
            return "SignatureVerificationException";
        } catch (InvalidClaimException e){
    
    
            // 获取token的服务器比使用token的服务器时钟快,请求分发到时间慢的服务器上导致时间还没到token的开始时间。token无效!失效的payload异常
            return "InvalidClaimException";
        } catch (AlgorithmMismatchException e){
    
    
            // token算法不一致
            return "AlgorithmMismatchException";
        } catch (TokenExpiredException e){
    
    
            // token过期
            return "TokenExpiredException";
        } catch (Exception exception) {
    
    
            return "otherException";
        }
    }

2.3 ShiroRealm

去掉了原来的 校验token的有效性token刷新(续签),然后修改了 认证 的方法。
PS:根据了解与个人理解,感觉token的续签应该放在JwtFilter 类中。

2.3.1 修改后的doGetAuthenticationInfo方法

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
    
    
        log.info("————身份认证 ————");
        // 这里的AuthenticationToken是用 JwtToken重写的实现方法getPrincipal()/getCredentials()都返回token
        String token = (String) auth.getCredentials();
        if (token == null) {
    
    
            log.info("————————身份认证失败——————————IP地址:  " + CommonUtils.getIpAddrByRequest(SpringUtils.getHttpServletRequest()));
            throw new AuthenticationException("token为空!");
        }

        // 解码获得username,用于查询数据库
        String username = JwtUtil.getUsername(token);
        if (username == null) {
    
    
            throw new AuthenticationException("token非法无效!");
        }
        // 查询用户信息
        SysUser sysUser = sysUserService.getUserByName(username);
        //判断账号是否存在
        if (sysUser == null) {
    
    
            throw new AuthenticationException("用户不存在!");
        }
        // 判断用户状态
        if (!"0".equals(sysUser.getDelFlag())) {
    
    
            throw new AuthenticationException("账号已被删除,请联系管理员!");
        }
        // 定义前缀+username 为缓存中的key,得到对应的value(cacheToken)
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + username));
        // 判断缓存中是否存在token
        if (CommonUtils.isNotEmpty(cacheToken)) {
    
    
            // 验证token
            if ("TokenExpiredException".equals(JwtUtil.verify(cacheToken))){
    
    
                throw new TokenExpiredException("token认证失效,token过期,重新登陆(人为抛出异常)");
            }else if ("success".equals(JwtUtil.verify(cacheToken))){
    
    
                long currentTime = System.currentTimeMillis();
                // token有效时间-当前时间↑ <= 需要刷新的有效时间?true需要刷新:false有效期内直接登录
                if (JwtUtil.checkRefresh(cacheToken,currentTime)){
    
    
                    // 2、濒死Token:Token未过期,已达到建议更换时间。
                    throw new TokenWillRefreshException("token认证即将失效,重新执行登陆返回新token(人为抛出异常)");
                }else {
    
    
                    // 1、正常Token:Token未过期,且未达到建议更换时间。
                    return new SimpleAuthenticationInfo(sysUser, cacheToken, getName());
                }
            }else if ("InvalidClaimException".equals(JwtUtil.verify(cacheToken))){
    
    
                throw new InvalidClaimException("token无效!失效的payload异常(Realm人为抛出异常)");
            }else if ("AlgorithmMismatchException".equals(JwtUtil.verify(cacheToken))){
    
    
                throw new AlgorithmMismatchException("token算法不一致(Realm人为抛出异常)");
            }else {
    
    
                throw null;
            }
        }
        return null;
    }

2.4 JwtFilter

先添加isLoginAttempt方法,再改写isAccessAllowed方法,然后添加refreshToken方法。

2.4.1 isLoginAttempt方法

    /**
     * 判断是否存在token(是否可以登录)
     * @param request  incoming ServletRequest
     * @param response outgoing ServletResponse
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    
    
        HttpServletRequest req= (HttpServletRequest) request;
        // 从请求头header中获取字段名为ACCESS_TOKEN的值(也就是我们说的token)
        String token=req.getHeader("ACCESS_TOKEN");
        return token !=null;
    }

2.4.2 改写的isAccessAllowed方法

	/**
     * 权限校验
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
        // 判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)){
    
    
            try {
    
    
                //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
                executeLogin(request, response);
                return true;
            }catch (Exception e){
    
    
                /*
                 * 注意这里捕获的异常其实是在Realm抛出的,但是由于executeLogin()方法抛出的异常是从login()来的,
                 * login抛出的异常类型是AuthenticationException,所以要去获取它的子类异常才能获取到我们在Realm抛出的异常类型。
                 * */
                Throwable cause = e.getCause();
                /*
                    TokenWillRefreshException:2、濒死Token:Token未过期,已达到建议更换时间。
                    TokenExpiredException:3、正常过期Token:Token已过期,但存在于缓存中。
                                           4、非正常过期Token:Token已过期,不存在于缓存中。
                 */
                if (cause!=null&&(cause instanceof TokenExpiredException || cause instanceof TokenWillRefreshException)){
    
    
                    //尝试去刷新token
                    String result = refreshToken(request, response);
                    if (result.equals("success")) {
    
    
                        return true;
                    }
                }
				// 如果不是以上情况,执行onAccessDenied
                return false;
            }
        }
        return false;
    }

2.4.3 refreshToken方法

   	/**
     * token续签
     * @param request
     * @param response
     * @return
     */
    private String refreshToken(ServletRequest request,ServletResponse response) {
    
    
        HttpServletRequest req= (HttpServletRequest) request;
        // 原因:拦截器在bean初始化前执行的,这时候redisUtil是null,需要通过下面这个方式去获取
        RedisUtil redisUtil= SpringUtils.getBean(RedisUtil.class);

        // 获取传递过来的accessToken
        // 从请求头header中获取字段名为ACCESS_TOKEN的值(也就是我们说的token)
        String token = req.getHeader("ACCESS_TOKEN");
        // 获取token里面的用户名
        String userName = JwtUtil.getUsername(token);
        // redis中的token 定义前缀+username 为缓存中的key,得到对应的value(cacheToken)
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + userName));

        // 判断refreshToken是否过期了
        if (CommonUtils.isNotEmpty(cacheToken)){
    
    
            // 判断是否超时
            long currentTime = System.currentTimeMillis();
            // 在这里进来,只可能属于
            // 2、濒死Token:Token未过期,已达到建议更换时间。
            // 3、正常过期Token:Token已过期,但存在于缓存中。
            // jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,
            // 程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
            // 重新sign,得到新的token(生成刷新的token)
            String newAuthorization = JwtUtil.sign(userName, currentTime);
            // 写入到缓存中,key不变,将value换成新的token
            redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + userName, newAuthorization);
            // 设置超时时间【这里除以1000是因为设置时间单位为秒了】
            redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + userName, JwtUtil.CACHE_EXPIRE_TIME / 1000);
            // 转换类型
            JwtToken jwtToken = new JwtToken(newAuthorization);
            try {
    
    
                // 提交给realm,再次让shiro进行认证
                getSubject(request, response).login(jwtToken);
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setHeader("ACCESS_TOKEN", newAuthorization);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", CommonConstant.ACCESS_TOKEN);
            }catch (Exception e){
    
    
                e.getMessage();
            }
            // 返回成功刷新标识
            return "success";
        }
        // 4、非正常过期Token:Token已过期,不存在于缓存中。
        // token认证失效,token过期,重新登陆
        return "token认证失效,token过期,重新登陆(JwtFilter)";
    }

猜你喜欢

转载自blog.csdn.net/weixin_42516475/article/details/130835425