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

news/2024/10/27 6:31:44/

前言

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");// 未授权界面返回JSONshiroFilterFactoryBean.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@Lazyprivate ISysUserService sysUserService; @Autowired@Lazyprivate RedisUtil redisUtil;/** 2.1 supports *//** 2.2 认证 *//** 2.3 校验token的有效性 *//** 2.4 token刷新(续签) *//** 2.5 授权 */}

2.1 supports

  • 代码:
	/*** 2.1 supports* 必须重写此方法,不然Shiro会报错*/@Overridepublic 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 实例*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {// 这里的AuthenticationToken是用 JwtToken重写的实现方法getPrincipal()/getCredentials()都返回tokenString 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,得到新的tokenString newAuthorization = JwtUtil.sign(userName, passWord);// 写入到缓存中,key不变,将value换成新的tokenredisUtil.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 权限信息*/@Overrideprotected 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.ppmy.cn/news/162287.html

相关文章

python PyAutoGUI 使用

pip install pyautoguidocument import pyautoguipyautogui.size() pyautogui.position()鼠标 To 是绝对坐标Rel 或者 不带 To 是相对坐标 pyautogui.moveTo(xNone, yNone, duration0.0) pyautogui.dragTo(xNone, yNone, duration0.0, buttonPRIMARY)click(xNone, yNone, cl…

uni-app之Cover-View组件详细使用教程

在 UniApp 中&#xff0c;Cover-View 组件是一种用于展示覆盖在页面上方的视图元素的组件。它可以用于创建各种遮罩、弹出层、悬浮按钮等效果&#xff0c;提供了更多自定义样式和交互的可能性。本教程将详细介绍 Cover-View 组件的用法和示例代码。 步骤1&#xff1a;创建一个…

宾得67中画幅相机红色版套装现身eBay

本文来自新摄影 近日&#xff0c;卖家shueido在eBay上正在销售一款红色宾得67中画幅相机套装。值得注意的是&#xff0c;这款套机机身颜色并非原装设计&#xff0c;而是玩家后期改装。据悉&#xff0c;eBay在售的这套宾得67相机套装包括宾得67机身、TTL取景器、105mm F2.4镜头以…

clang 01. clang driver流程分析

文章目录 前言在这里简要概述一下clang的流程 1.clang driver代码分析1.1创建诊断&#xff08;DIagnosticsEngine&#xff09;实例1.2创建Driver(clang::driver::Driver)的实例1.3通过Driver的BuildCompilation方法生成需要执行的命令1.4Jobs构建完成&#xff0c;通过Driver的E…

Python3中对时间的处理(持续更新ing...)

诸神缄默不语-个人CSDN博文目录 本文介绍Python3中各种处理时间的库和使用方案 最近更新时间&#xff1a;2023.6.2 最早更新时间&#xff1a;2023.6.2 文章目录 1. datetime库2. time库3. JioNLP库&#xff1a;&#xff08;中文&#xff09;从文本中提取时间信息4. datefinde…

深入了解JavaScript中的Promise

在JavaScript中&#xff0c;异步编程是必不可少的。过去&#xff0c;我们通常使用回调函数来处理异步操作&#xff0c;但回调地狱&#xff08;callback hell&#xff09;和复杂的错误处理使得代码难以维护。为了解决这些问题&#xff0c;ES6引入了Promise&#xff0c;它是一种更…

Java --- springboot3之日志管理

目录 一、日志整合原理 1.1、简介 1.2、日志格式 1.3、记录日志 1.4、日志级别 1.5、日志分组 1.6、日志文件输出 1.7、文件归档与滚动切割 1.8、自定义日志系统 一、日志整合原理 规范&#xff1a;项目开发不要编写System.out.println()&#xff0c;应该用日志记录信息…

如何进行网站建设定制开发

如今&#xff0c;由于互联网的迅速发展&#xff0c;市场竞争越来越激烈&#xff0c;企业想要在市场中保持竞争力&#xff0c;就必须要有自己的品牌形象。而网站建设则是品牌建设的重要组成部分&#xff0c;一个好的网站能够有效提升企业品牌形象&#xff0c;提高客户满意度和忠…