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

news2024/10/7 18:25:20

前言

之前在写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)";
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/600788.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

绝地求生 压q python版

仅做学习交流&#xff0c;非盈利&#xff0c;侵联删&#xff08;狗头保命) 一、概述 1.1 效果 总的来说&#xff0c;这种方式是通过图像识别来完成的&#xff0c;不侵入游戏&#xff0c;不读取内存&#xff0c;安全不被检测。 1.2 前置知识 游戏中有各种不同的q械&#xf…

深入浅出:FFmpeg 音频解码与处理全解析

深入浅出&#xff1a;FFmpeg 音频解码与处理全解析 一、FFmpeg 简介1.1 FFmpeg 的历史与发展1.2 FFmpeg 的主要组成部分 二、音频编解码基础 (Basics of Audio Encoding and Decoding)2.1 音频编解码的原理 (Principle of Audio Encoding and Decoding)2.1.1 采样 (Sampling)2.…

chatgpt赋能python:Python函数调用局部变量-深入了解

Python函数调用局部变量-深入了解 函数调用局部变量是Python中的一个重要概念&#xff0c;特别是在大型项目中&#xff0c;其中多个函数共享相同变量时。在本文中&#xff0c;我们将深入探讨Python函数调用局部变量&#xff0c;并为您介绍一些实用技巧。 什么是Python函数调用…

chatgpt赋能Python-python函数计算器

简介 Python是一种高级编程语言&#xff0c;它在数据科学和机器学习等领域非常流行。但是&#xff0c;很多人可能不知道它也可以用来编写简单的函数计算器。 在本文中&#xff0c;我们将介绍一些基本的Python函数&#xff0c;并教你如何使用它们来编写一个简单但强大的函数计…

C 语言里面的 extern “C“ ,并没有那么简单!

前言 本文详细解析extern "C"的底层原理与实际应用。在你工作过的系统里&#xff0c;不知能否看到类似下面的代码。 这好像没有什么问题&#xff0c;你应该还会想&#xff1a;“嗯⋯是啊&#xff0c;我们的代码都是这样写的&#xff0c;从来没有因此碰到过什么麻烦啊…

CTEX中使用winEdt排版编辑插入图片.eps应用排版举例

CTEX中使用winEdt排版编辑插入图片.eps应用排版举例 在使用WinEdt进行排版编辑的时候&#xff0c;可以对文档格式排版等灵活快速排版&#xff0c;并实现pdf的文档的生成。本文将举例说明在WinEdt中&#xff0c;插入图片的方法的排版举例应用。 一、具体方法步骤 1.根据已有图片…

配置OCI上数据库服务的EM Database Express

本文参考了以下文档&#xff1a; DBCS: How To Setup EM Express in Bare Metal and Virtual Machine DB Systems(OCI) (Doc ID 2453454.1)Oracle Database 12c: EM Database ExpressAccess the Database Home Page in EM Database Express 第1步&#xff1a;数据库中设端口 …

【嵌入式烧录/刷写文件】-2.8-Hex文件转换为S19文件

案例背景(共5页精讲)&#xff1a; 有如下一段Hex文件&#xff0c;将其转换为Motorola S-record(S19/SREC/mot/SX)文件。 :2091000058595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F70717273747576775F :2091200078797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939…

​ACL 2023 | 用二分类解决无监督常识问答

常识推理最近在自然语言处理研究中引起了广泛关注&#xff0c;现有的常识推理方法一般分为两种&#xff1a;一种是在开源数据集上对大型语言模型进行微调 [1]&#xff0c;另一种是借助外部知识图谱来训练。然而&#xff0c;构建有标注的常识推理数据集既昂贵&#xff0c;又受限…

uCOSii消息队列

消息队列管理(MESSAGE QUEUE MANAGEMENT) 1、消息队列定义 消息队列好比人们排队买票&#xff0c;排队的人好比是消息&#xff0c;每来一个人&#xff0c;都要到队伍的尾部去排队&#xff0c;叫添加一条消息到队列中。售票员卖票给先到的人&#xff0c;叫从对列中接收一条消息…

Talk预告 | 罗格斯大学徐子昊:在域迁移学习中,用变分推理自动生成可解释的域索引

本期为TechBeat人工智能社区第501期线上Talk&#xff01; 北京时间5月31日(周三)20:00&#xff0c;罗格斯大学 在读博士生—徐子昊的Talk将准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “在域迁移学习中&#xff0c;用变分推理自动生成可解释的域索引…

Expeditors EDI需求详解

Expeditors是一家全球性的物流公司&#xff0c;成立于1979年&#xff0c;总部位于美国华盛顿州的西雅图。该公司提供海运、空运、货运代理、清关、仓储等一系列全球物流服务&#xff0c;并致力于通过数字化技术提高供应链的可见性和效率。Expeditors的客户遍及各行各业&#xf…

CMAKE变量与选择详解

目录 在 CMake 中&#xff0c;变量和选项是&#xff1a; CMake中的变量&#xff1a; 接下来是一个cmake的案例&#xff1a; 在CMake中定义和使用函数&#xff1a; 在 CMake 中&#xff0c;变量和选项是&#xff1a; 变量&#xff08;Variables&#xff09;&#xff1a; CMak…

chatgpt赋能python:Python函数查看快捷键:不可或缺的工具

Python函数查看快捷键&#xff1a;不可或缺的工具 Python是一门流行的编程语言&#xff0c;是数据分析、机器学习、人工智能等各种领域的首选语言。对于有10年Python编程经验的开发人员来说&#xff0c;Python函数查看快捷键可能是最熟悉的工具之一。因此&#xff0c;本篇文章…

k8s部署docker

1 环境准备 操作系统&#xff1a;centos7.9_x64 Docker&#xff1a;20-ce K8s&#xff1a;1.23 操作系统最小硬件配置&#xff08;在vmmare安装时需要选择&#xff09;&#xff1a;2核CPU、2G内存、20G硬盘 k8s-master&#xff1a;192.168.88.191 k8s-node1&#xff1a;192.…

chatgpt赋能python:Python冒泡排序算法详解

Python冒泡排序算法详解 介绍 冒泡排序是一种简单但相对较慢的排序算法。这个算法会重复地遍历要排序的数列&#xff0c;每次比较两个元素&#xff0c;如果它们的顺序错误就交换它们的位置&#xff0c;直到没有任何一对元素需要交换为止。这个算法由于排序过程中最大元素就像…

怎么用Excel VBA写一个excel批量合并的程序?

您可以按照以下VBA代码来实现把同一路径上的所有工作簿合并到同一个工作簿中&#xff1a; VBA Option Explicit Sub MergeWorkbooks() Dim path As String, fileName As String, sheet As Worksheet Dim targetWorkbook As Workbook, sourceWorkbook As Workbook Dim workshe…

建立可重复使用的自动测试过程

建立可重复使用的自动测试过程 在软件开发领域&#xff0c;自动化测试已经成为必不可少的一部分&#xff0c;它可以提高软件产品的质量、减少错误率、加快测试时间。但是&#xff0c;为了让自动测试过程更加高效和可重复使用&#xff0c;需要建立一套稳定的自动化测试框架。 自…

ES6: 模板字符串和箭头函数的基本使用

前言 本文主要介绍了ES6中模板字符串和箭头函数的基本使用 一、模板字符串 1、基本介绍 由反引号(在键盘Esc键的下面&#xff09;圈住的字符串即模板字符串举例&#xff1a; //普通字符串 const namehello console.log(name)//模板字符串 const name1world console.log(na…

前端js实现将数组某一项符合条件的对象,放到首位

哈喽 大家好啊 在日常前端开发需求中&#xff0c;总是会遇到开发数组&#xff0c;将某一项对象值&#xff0c;放到首位&#xff0c;让用户更好的去选择&#xff0c;比如省会城市优先等 我做的案例是需要将地区中的四川放到首位 以下是我的代码&#xff1a; 简单说明下思路&a…