【shiro】shiro整合JWT——2.如何整合

news2025/1/11 12:47:23

前言

shiro整合JWT系列,主要记录核心思路–如何在shiro+redis整合JWTToken。
上一篇中,我们知道了需要创建JwtToken、JwtUtil、JwtFilter。
该篇主要讲如何在shiro框架中,配置Jwt。
ps:本文主要以记录核心思路为主。

1、ShiroConfig配置

  • 核心片段代码:
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
        filterChainDefinitionMap.put("/", "anon");

        // 添加自己的过滤器并且取名为jwt 核心部分
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
        filterChainDefinitionMap.put("/**", "jwt");

        // 未授权界面返回JSON
		shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
        shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

在ShiroConfig中,创建了一个filterMap,里面就存储了("jwt", new JwtFilter())的键值对,并作为Fileters设置到shiroFilterFactoryBean中,在最后put到filterChainDefinitionMap里进行拦截。

疑惑:有人感觉这里怪怪的,但又说不出来,到底哪里不一样了?
回答:原本shiro在最后是filterChainDefinitionMap.put("/**", "authc");,让剩下的请求必须登录认证;现在这里改成了自定义的filterChainDefinitionMap.put("/**", "jwt");,意味着,剩下的请求全都交给JwtFilter类来处理。

2、ShiroRealm配置

@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
	// 下面这个两个类,这里就不给出具体内容了,作用在下面一目了然
    @Autowired
    @Lazy
    private ISysUserService sysUserService; 
    @Autowired
    @Lazy
    private RedisUtil redisUtil;

    /** 2.1 supports */
    
	/** 2.2 认证 */

	/** 2.3 校验token的有效性 */
	
	/** 2.4 token刷新(续签) */

	/** 2.5 授权 */

}

2.1 supports

  • 代码:
	/**
     * 2.1 supports
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

我们可以看到Realm提供的接口supports,原本是由AuthenticatingRealm实现的。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述 在这里插入图片描述
上图中,AuthenticatingRealm的getAuthenticationTokenClass方法默认值是UsernamePasswordToken.class,xxx.isAssignableFrom是判断传入的类cls能否(通过标识转换或扩展引用转换转换)转换为xxx对象表示的类型。

isAssignableFrom是Class.java中的方法,其解释:

确定此class对象所表示的类或接口是否与指定的class参数所表示的类别或接口相同,或者是该类别或接口的超类别或超接口。如果是,则返回true;否则返回false。如果此Class对象表示基元类型,则如果指定的Class参数正是此Class对象,则此方法返回true;否则返回false。【百度翻译】

  • 总结:
    1.原来的就是token的String类型是否能转换成getAuthenticationTokenClass方法中的类型(UsernamePasswordToken.class)。
    2.这里重写supports方法,return token instanceof JwtToken;判断token是否属于JwtToken类型,就不用原来默认的判断了。

2.2 认证 (doGetAuthenticationInfo)

  • 代码:
	/**
     * 2.2 认证
     * @param auth 用户身份信息 token
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        // 这里的AuthenticationToken是用 JwtToken重写的实现方法getPrincipal()/getCredentials()都返回token
        String token = (String) auth.getCredentials();
        if (token == null) {
            log.info("————————身份认证失败——————————");
            throw new AuthenticationException("token为空!");
        }
        // 校验token有效性
        SysUser loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

看到这里,会发现这段代码和以往的shiro差距还蛮大的,可以对比【Shiro】SimpleAuthenticationInfo如何验证password中自定义的ShiroRealm类给出的doGetAuthenticationInfo方法;但是其实变化不是很大,请先继续往下看checkUserTokenIsEffect方法

2.3 校验token的有效性(checkUserTokenIsEffect)

  • 代码:
	/**
     * 2.3 校验token的有效性
     *
     * @param token
     */
    public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解码获得username,用于查询数据库
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }
        // 查询用户信息
        SysUser loginUser = new SysUser();
        SysUser sysUser = sysUserService.getUserByName(username);
        //判断账号是否存在
        if (sysUser == null) {
            throw new AuthenticationException("用户不存在!");
        }
        // 校验token是否超时失效 & 或者账号密码是否错误 核心部分
        if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
            throw new AuthenticationException("Token失效请重新登录!");
        }
        // 判断用户状态
        if (!"0".equals(sysUser.getDelFlag())) {
            throw new AuthenticationException("账号已被删除,请联系管理员!");
        }
        // 复制对象,为什么要这么做?麻烦懂的大佬留言指教一下
        BeanUtils.copyProperties(sysUser, loginUser);
        return loginUser;
    }

从整体的角度看下,会发现这个部分的大体逻辑和以前的shiro差不多,我们来看下他们的异同:

  • 相同部分:
    用AuthenticationToken对象获取用户名(账号),然后根据用户名查询数据库,得到该用户的User对象(用户账号,加密密码,盐值等等)。
    PS:这些逻辑只是被抽象成一个新的方法checkUserTokenIsEffect。
  • 区别部分:
    1、以前AuthenticationToken是存放username和password信息的,现在是token字符串。
    2、相比以前,现需要多考虑token的有效性(具体看2.4 jwtTokenRefresh),也就出现了大家常听到的token续签

2.4 token刷新(jwtTokenRefresh)

前提了解:

JWTToken刷新生命周期 (解决用户一直在线操作,提供Token失效问题)
1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
3、当该用户这次请求JWTToken值还在生命周期内,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样)
4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
5、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
6、每次当返回为true情况下,都会给Response的Header中设置Authorization,该Authorization映射的v为cache对应的v值。
7、注:当前端接收到Response的Header中的Authorization值会存储起来,作为以后请求token使用
参考方案:https://blog.csdn.net/qq394829044/article/details/82763936

  • 代码:
	/**
     * 2.4 token刷新
     *
     * @param token
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        // 定义前缀+token 为缓存中的key,得到对应的value(cacheToken)
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        // 判断缓存中的token是否存在
        if (cacheToken != null && !cacheToken.equals("") && !cacheToken.equals("null")) {
            // 校验token有效性
            // 缓存中存在,验证失败(JwtUtil.verify在上一篇文章中已经介绍)
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                // 重新sign,得到新的token
                String newAuthorization = JwtUtil.sign(userName, passWord);
                // 写入到缓存中,key不变,将value换成新的token
                redisUtil.set("PREFIX_TOKEN" + token, newAuthorization);
                // 设置超时时间【这里除以1000是因为设置时间单位为秒了】【一般续签的时间都会乘以2】
                redisUtil.expire("PREFIX_TOKEN" + token, JwtUtil.EXPIRE_TIME / 1000);
            // 缓存中存在,验证成功
            } else {
             	// 上面的写法,与下面的相同
                // 用户这次请求JWTToken值还在生命周期内,重新put新的生命周期时间(有效时间)
                redisUtil.set("PREFIX_TOKEN" + token, cacheToken, JwtUtil.EXPIRE_TIME / 1000);
            }
            return true;
        }
        return false;
    }

PS:开篇代码已经提到redisUtil和sysUserService不花篇幅说明,用到的方法在代码中已经有相应的解释。

2.5 授权(doGetAuthorizationInfo)

  • 代码:
	/**
     * 2.5 授权
     *
     * @param principals token
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("————权限认证 [ roles、permissions]————");
        SysUser sysUser = null;
        String username = null;
        if (principals != null) {
            sysUser = (SysUser) principals.getPrimaryPrincipal();
            username = sysUser.getUserName();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 设置用户拥有的角色集合,比如“admin,test”
        Set<String> roleSet = sysUserService.getUserRolesSet(username);
        info.setRoles(roleSet);

        // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
        Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
        info.addStringPermissions(permissionSet);
        return info;
    }
  • 注意:
    我们这里使用的redis依赖是spring-boot-starter-data-redis;上面代码中sysUserService查询用户角色和权限集合,需要在这些方法的实现类上加上@Cacheable(value = "用来指定缓存组件的名字", key = "缓存数据时使用的 key"),以此来将查询的信息存入缓存中。

疑惑:一定要用@Cacheable吗?直接用redis的操作加入缓存可以吗?
回答:不一定;可以。@Cacheable是Spring的注解,实现了很多缓存的方法,具体可以看SpringBoot 缓存之 @Cacheable 详细介绍。

3、token执行的流程图

在这里插入图片描述
这里解释以下图中第2步和第6步:

  • 第2步:Controller层处理的 4缓存token,我的测试过程中是使用了redis,在生成完token后,会将其token存入到redis中。
  • 第6步:有些人会问为什么是无状态登录?还有人会问使用token就是为了无状态登录,为什么还需要结合redis缓存?和用session有什么区别?
    • 解释:
      1、有状态登录:是用户请求的时候,在服务器上已经缓存(利用session缓存)了用户的信息,cookie存储在客户端,session存储在服务端。
      无状态登录:是用户的信息不在服务端进行存储,只将token存储在客户端。
      2、使用redis的目的是为了后续的分布式支持,应对高并发的扩展性(集群模式),性能好,同时使用上变得更加灵活。(目前才刚了解,还不太熟悉)
      3、redis 的性能要比传统的 session 存储方式更高效,因为 redis 是基于内存的,而且支持异步方式存储数据。除了性能上的区别,就是更加的灵活,如:

    1、token+redis 方案,服务器可以清除 redis 中对应的 token,这样就可以在服务器端对该指定用户进行注销下线了。
    2、token+session+redis 方案,拿session用来存储 token,然后再将 session 存储在 redis 中。这样有个好处,就是一个 session 可以存储多个 token,可以让同一个账户在多设备端共用一个会话。
    session,cookie,token,redis
    Session机制详解及分布式中Session共享解决方案

4、简单整理:

  • Old:
    以前的登录,在controller层的登录接口执行subject.login(token),然后就执行到doGetAuthenticationInfo方法,这里的token为UsernamePasswordToken。在doGetAuthenticationInfo中主要是从数据库中查出用户对象密码盐值,然后加上realm名字放入SimpleAuthenticationInfo对象中,用于assertCredentialsMatch中的info进行验证

  • New:
    整合Jwt后,在controller层的登录接口不执行subject.login(token),只是生成token返回给前端。后面所有的请求,前端都将在header中放入token,每一次被JwtFilter拦截下来的请求,都将会执行到executeLogin方法中的getSubject(request, response).login(jwtToken);实现本次请求的登录(每一次请求都得执行一次实际登录代码,而不是Controller层的登录接口),在该方法执行完后,会执行到Reealm中的doGetAuthenticationInfo方法,这里主要作为token的验证或续签(如上面介绍的checkUserTokenIsEffect和jwtTokenRefresh)

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

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

相关文章

如何零基础自学黑客?

我经常会看到这一类的问题&#xff1a; 学习XXX知识没效果&#xff1b;学习XXX技能没方向&#xff1b;学习XXX没办法入门&#xff1b; 给大家一个忠告&#xff0c;如果你完全没有基础的话&#xff0c;前期最好不要盲目去找资料学习&#xff0c;因为大部分人把资料收集好之后&…

Android中的WorkManager

Android中的WorkManager 在后台运行任务会消耗设备有限的资源&#xff0c;如RAM和电池。这可能会导致用户体验不佳。例如&#xff0c;后台任务可能会降低设备的电池寿命或用户在观看视频、玩游戏、使用相机等时可能会遇到设备性能不佳的情况。 为了提高电池性能&#xff0c;An…

关于人力资源管理职能,你需要知道的事

每个成功的企业都有一个称职的人力资源部门。它是任何企业的重要组成部分&#xff0c;是员工和管理层之间的纽带。人力资源涵盖影响组织人员的所有任务&#xff0c;从基本的人力资源活动到战略决策。 对于任何希望可持续发展的企业来说&#xff0c;人力资源管理职能的重要性不…

0802数量积向量积混合积-向量代数与空间解析几何

文章目录 1 两向量的数量积1.1 引例1.2 定义1.3 推论1.4 运算规律1.4 数量积的坐标表示 2 两向量的向量积2.1 定义2.2 重要结论2.3 几何意义&#xff08;向量积模&#xff09;2.4 向量积的运算规律2.5 向量积的坐标表示 3 向量的混合积3.1 混合积的定义3.2 混合积的坐标表示3.3…

初识报表引擎-FineReport

简介 提到报表引擎大家可能都会说帆软。目前商用的比较突出的两个报表引擎&#xff1a;分别是帆软FineReport、RDP报表引擎&#xff0c;其中帆软功能突出且非常完整但是价格较高&#xff0c;RDP功能相对完整但是不够强大貌似还有些BUG&#xff0c;不过价格很低。就目前的情况来…

《模板的进阶》

本文主要介绍C模板知识&#xff0c;包括模板的参数类型&#xff0c;模板的特化&#xff0c;模板的分离编译 文章目录 思维导图一、非类型模板参数二、模板的特化2.1模板特化的概念2.2函数模板特化2.3类模板的特化2.3.1全特化2.3.2偏特化 2.4非类型模板参数也是可以特化的 三、模…

Android 调用系统隐藏的类和方法

1.Android系统隐藏的类和方法 阅读Android源码时&#xff0c;会发现很多被UnsupportedAppUsage注解的方法&#xff0c;这些方法不能被外部应用访问。 比如Android中的PackageParser类&#xff0c;这个类是在android.content.pm包下面&#xff1a; 可以看到这个类是隐藏的&…

有哪些自动化构建工具推荐? - 易智编译EaseEditing

以下是几个常用的自动化构建工具推荐&#xff1a; Jenkins&#xff1a; Jenkins 是一个开源的自动化构建工具&#xff0c;广泛用于持续集成和持续交付。 它支持各种编程语言和版本控制系统&#xff0c;并提供了丰富的插件生态系统&#xff0c;可实现灵活的构建流程和自动化部…

【Spring】— 动态SQL :<choose>、<when>和<otherwise>元素

<choose>、<when>和<otherwise>元素 在使用<if>元素时&#xff0c;只要test属性中的表达式为true&#xff0c;就会执行元素中的条件语句&#xff0c;但是在实际应用中&#xff0c;有时只需要从多个选项中选择一个执行。例如&#xff0c;若用户姓名不为…

九、Docker网络

Docker网络 一、docker网络介绍 Docker网络在Docker的基础知识中算比较重要的了&#xff0c;需要多多实验理解。 Docker服务安装启动后默认在host上创建了三个网络&#xff1a; [rootk8s-m1 ~]# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UN…

毛毛莫名的大一生活总结

毛毛莫名的大一生活总结 1. 关于高考我的看法1.1 初中1.2 高中 2. 大一开学前的暑假3. 大一开学3.1 军训3.2 学生会 学校社团 运动团体3.2.1 学生会3.2.1.1 院学生会3.2.1.2校学生会 3.2.2 社团3.2.3 运动团体 4. 大学生活 1. 关于高考我的看法 1.1 初中 初一初二对学习不用太…

一种简单的Android骨架屏实现方案----0侵入0成本

对骨架屏的理解 什么是骨架屏 所谓骨架屏&#xff0c;就是在页面进行耗时加载时&#xff0c;先展示的等待 UI, 以告知用户程序目前正在运行&#xff0c;稍等即可。 等待的UI大部分是 loading 转圈的弹窗&#xff0c;有的是自己风格的小动画。其实大同小异。而骨架屏无非也是一…

本地部署github上的stable diffuion,轻松玩转ai绘画(新手小白也能懂)

你也想自己生成上面这样好看的图片吗&#xff1f;废话不多说&#xff0c;跟着博主&#xff0c;按步骤来&#xff0c;做完你也就可以了&#xff0c;而且无任何限制&#xff0c;懂得都懂&#xff01;&#x1f60e; 目录 第一步&#xff1a;准备VPN 第二步&#xff1a;安装Pyth…

LabVIEWCompactRIO 开发指南第七章48

LabVIEWCompactRIO 开发指南第七章48 5.如果控制器上已经安装了LabVIEW实时和NI-RIO&#xff0c;请选择自定义软件安装并点击下一步。如果出现警告对话框&#xff0c;请单击“是”。单击NI-工业通信用于EtherCAT的框。将自动检查所需的依赖项。单击下一步继续在控制器上安装软…

一款开源的无线CMSIS DAP ARM芯片下载调试器详细说明

文章目录 概要1. 一般概念1.1 CMSIS—DAP的一般概念1.2 支持的芯片1.3 典型应用场景 2. 原理图与尺寸图2.1 Host端&#xff08;发送端&#xff09;原理图2.2 Target&#xff08;目标&#xff09;端原理图2.3 Host尺寸图2.4 Target尺寸图2.5 实物图 3. 使用方法3.1 连接方法3.1.…

群晖折腾记1—群晖NAS使用docker中的ddns-go,DDNS阿里云ali实现Ipv6访问

群晖折腾记1—群晖NAS使用docker中的ddns-go,DDNS阿里云ali实现Ipv6访问 前置条件科普时间具体步骤1. 域名购买2. 获取AccessKey ID和Secret3. 获取免费SSL证书4. 在docker中运行ddns-go6.设置反向代理 前置条件 1、移动宽带只有IPv6公网地址&#xff0c;若你的宽带为电信或联…

Eslint配置指南

ESLint最初是由Nicholas C. Zakas 于2013年6月创建的开源项目。ESLint 是一个开源的 JavaScript 代码检查工具&#xff0c;它是用来进行代码的校验&#xff0c;检测代码中潜在的问题&#xff0c;比如某个变量定义了未使用、函数定义的参数重复、变量名没有按规范命名等等。 中…

logging 模块因权限问题写入日志失败

哈喽大家好&#xff0c;我是咸鱼 今天跟大家分享一个使用 Python 的 logging 模块写入日志文件时遇到的权限问题&#xff0c;不知道你们有没有遇到过 1.案例现象 今天上班的时候手机短信收到了 zabbix 告警&#xff0c;但是发现了不对劲的地方&#xff1a;微信没有收到告警信…

【C++】虚函数相关常见问题

【C】虚函数相关常见问题 文章目录 【C】虚函数相关常见问题1.说说为什么要虚析构&#xff1f;2. C默认的析构函数为什么不是虚函数?3. 构造函数能不能是虚函数4. 说说什么是虚继承&#xff0c;解决什么问题&#xff0c;如何实现&#xff1f;5. 说说什么是虚函数6.说说虚函数的…

VESC操作入门——PPM输入控制和ADC输入控制

目录 一、PPM输入控制1.1、硬件准备1.2、PPM信号1.3、校准电机1.4、输入设置 二、ADC输入控制2.1、硬件准备2.2、更改固件2.3、电压信号2.4、校准电机2.5、输入设置 三、电动车转把控制3.1、转把说明3.2、转把测试 ODrive、VESC和SimpleFOC 教程链接汇总&#xff1a;请点击 一、…