解决Springboot整合Shiro+Redis退出登录后不清除缓存

解决Springboot整合Shiro+Redis退出登录后不清除缓存

  • 问题发现
  • 问题解决

问题发现

如果再使用缓存管理Shiro会话时,退出登录后缓存的数据应该清空。

依赖文件如下:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.18</version>
</dependency>
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-web-starter</artifactId><version>1.13.0</version>
</dependency>

示例代码如下:

@Controller
@RequestMapping(value = "/user")
public class UserController {@PostMapping("/login")public ModelAndView login(HttpServletRequest request, @RequestParam("username") String username, @RequestParam("password") String password) {// 提前加密,解决自定义缓存匹配时错误UsernamePasswordToken token = new UsernamePasswordToken(username,//身份信息password);//凭证信息ModelAndView modelAndView = new ModelAndView();// 对用户信息进行身份认证Subject subject = SecurityUtils.getSubject();if (subject.isAuthenticated() && subject.isRemembered()) {modelAndView.setViewName("redirect:main");return modelAndView;}try {subject.login(token);// 判断savedRequest不为空时,获取上一次停留页面,进行跳转
//            SavedRequest savedRequest = WebUtils.getSavedRequest(request);
//            if (savedRequest != null) {
//                String requestUrl = savedRequest.getRequestUrl();
//                modelAndView.setViewName("redirect:"+ requestUrl);
//                return modelAndView;
//            }} catch (AuthenticationException e) {e.printStackTrace();modelAndView.addObject("responseMessage", "用户名或者密码错误");modelAndView.setViewName("redirect:index");return modelAndView;}System.out.println(subject.getSession().getId());System.out.println(subject.isAuthenticated());modelAndView.setViewName("redirect:main");return modelAndView;}@GetMapping("/logout")public void logout() {SecurityUtils.getSubject().logout();}
}

自定义Realm,示例代码如下:

@Component
public class UserRealm extends AuthorizingRealm {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@Autowiredprivate PermissionService permissionService;@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {String username = (String) authenticationToken.getPrincipal();String password = new String((char[]) authenticationToken.getCredentials());User user = userService.getOne(new QueryWrapper<User>().ge("username", username));if (user == null) {throw new UnknownAccountException("账号不存在");}Sha256Hash sha256Hash = new Sha256Hash(password, username);if (!sha256Hash.toHex().equals(user.getPassword())) {throw new IncorrectCredentialsException("密码错误");}SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, sha256Hash.toHex(), new ByteSourceSerializable(username), getName());return simpleAuthenticationInfo;}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {User user = (User) principalCollection.getPrimaryPrincipal();List<Role> roleList = roleService.getByUserId(user.getId());SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();roleList.forEach(item ->{simpleAuthorizationInfo.addRole(item.getName());});List<Integer> roleIds = roleList.stream().map(Role::getId).collect(Collectors.toList());List<Permission> permissions = permissionService.listByIds(roleIds);permissions.forEach(item->{simpleAuthorizationInfo.addStringPermission(item.getName());});return simpleAuthorizationInfo;}
}

Config配置文件如下:

package org.example.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.example.realm.UserRealm;
import org.example.shiroTest.CustomSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.client.RestTemplate;import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.servlet.Filter;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;/*** packageName org.example.config** @author shanchengwei* @className ShiroConfig* @date 2024/11/28*/
@Configuration
public class ShiroConfig {/*** 核心安全过滤器对进入应用的请求进行拦截和过滤,从而实现认证、授权、会话管理等安全功能。*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 当未登录的用户尝试访问受保护的资源时,重定向到这个指定的登录页面。shiroFilterFactoryBean.setLoginUrl("/user/index");// 成功后跳转地址,但是测试时未生效shiroFilterFactoryBean.setSuccessUrl("/user/main");// 当用户访问没有权限的资源时,系统重定向到指定的URL地址。shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");// 配置拦截器链,指定了哪些路径需要认证、哪些路径允许匿名访问Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();filterChainDefinitionMap.put("/user/login", "anon");filterChainDefinitionMap.put("/user/logout", "logout");filterChainDefinitionMap.put("/**", "authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*** 创建Shiro Web应用的整体安全管理*/@Beanpublic DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm());defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注册会话管理
//        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());// 可以添加其他配置,如缓存管理器、会话管理器等return defaultWebSecurityManager;}/*** 创建会话管理*/@Beanpublic DefaultWebSessionManager defaultWebSessionManager() {DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();defaultWebSessionManager.setGlobalSessionTimeout(10000);
//        defaultWebSessionManager.setSessionDAO(sessionDAO());defaultWebSessionManager.setCacheManager(cacheManager()); // 设置缓存管理器,自动给sessiondao赋值return defaultWebSessionManager;}@Beanpublic SessionDAO sessionDAO() {RedisSessionDao redisSessionDao = new RedisSessionDao();redisSessionDao.setActiveSessionsCacheName("shiro:session");return redisSessionDao;}/*** 指定密码加密算法类型*/@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();hashedCredentialsMatcher.setHashAlgorithmName("SHA-256"); // 设置哈希算法return hashedCredentialsMatcher;}/*** 注册Realm的对象,用于执行安全相关的操作,如用户认证、权限查询*/@Beanpublic Realm realm() {UserRealm userRealm = new UserRealm();userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法userRealm.setCachingEnabled(true); // 启动全局缓存userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存userRealm.setCacheManager(cacheManager());return userRealm;}@Beanpublic CacheManager cacheManager() {RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());return redisCacheManage;}@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic RedisTemplate<String, Object> redisTemplate() {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper objectMapper = new ObjectMapper();//设置了 ObjectMapper 的可见性规则。通过该设置,所有字段(包括 private、protected 和 package-visible 等)都将被序列化和反序列化,无论它们的可见性如何。objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);//启用了默认的类型信息 NON_FINAL 参数表示只有非 final 类型的对象才包含类型信息,这可以帮助在反序列化时正确地将 JSON 字符串转换回对象。objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();//key采用String的序列化方式redisTemplate.setKeySerializer(stringRedisSerializer);//hash的key也采用String的序列化方式redisTemplate.setHashKeySerializer(stringRedisSerializer);return redisTemplate;}
}

当我点击退出登录后报错,如图所示:

在这里插入图片描述
后台日志报错,如图所示:

在这里插入图片描述

Redis保存数据,如图所示:

在这里插入图片描述

问题解决

根据报错可以知道,User对象无法转换为String字符串,就很神奇,存进去和删除的时候为什么参数不一致哦,然后就开启了Debug模式,一步步排查。

调用logout()方法,进入DefaultSecurityManager类,如图所示:

在这里插入图片描述

最后进入CachingRealm类,如图所示:
在这里插入图片描述
根据Debug先进入AuthorizingRealm类(前面介绍过缓存没保存授权的记录,不做讲解,参考AuthenticatingRealm),实际是再AuthenticatingRealm.doClearCache(),然后获取缓存和凭证进行删除操作,如图所示:
在这里插入图片描述

然后我们看下这个Key是如何获取的,实际上也是拿的凭证信息,如图所示:
在这里插入图片描述

然后就联想到这个凭证信息再自定义Realm中存放的,然后我就将凭证中的信息改成了username字段,示例代码如下:

SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), sha256Hash.toHex(), new ByteSourceSerializable(username), getName());

AuthenticatingRealm中的Redis数据删除后返回到AuthorizingRealm类,继续执行该类的缓存清除(虽然没有缓存数据),如图所示:

在这里插入图片描述

然后就报错了,如图所示:

在这里插入图片描述
我们可以看到又是一个类型转换错误,再getAuthorizationCacheKey()方法中直接将对象返回,如图所示:

在这里插入图片描述

解决该问题的方法有两种:

  • 方法一:子类重写该方法,自定义的Realm中去重写,示例代码如下:
@Component
public class UserRealm extends AuthorizingRealm {// 省略其它代码... ...@Overrideprotected Object getAuthorizationCacheKey(PrincipalCollection principals) {return principals.getPrimaryPrincipal();}
}
  • 方法二:再Config文件中不启用授权的缓存,这样缓存为null,就不会往下走,示例代码如下:
    @Beanpublic Realm realm() {UserRealm userRealm = new UserRealm();userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法userRealm.setCachingEnabled(true); // 启动全局缓存userRealm.setAuthorizationCachingEnabled(false); // 启动授权缓存userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存userRealm.setAuthenticationCacheName("Authentication");userRealm.setCacheManager(cacheManager());return userRealm;}

这两种方式都可以解决类型转换的错误。

解决了这个删除的问题我们再回到最前面的问题:存进去和删除的时候为什么参数不一致哦?

我们进入login()方法,如图所示:
在这里插入图片描述
进入authenticate()方法,最终进入AuthenticatingRealm类的getAuthenticationInfo()方法,如图所示:
在这里插入图片描述
第一次判断缓存为空,进入自定义Realm中查询数据,然后将查询的数据再放入缓存中,如图所示:
在这里插入图片描述
我们看下getAuthenticationCacheKey()方法是如何获取key的,如图所示:

在这里插入图片描述
可以看见直接获取的参数getPrincipal()方法,也就是UsernamePasswordToken中的username字段,如图所示:

在这里插入图片描述

到此也就知道为什么存的时候和删的时候,Key值不一致的原因。

这样又带来了另外一个问题:用username当凭证就会每次都要去查询,非常的繁琐,有没有什么好的办法?还真有,我们知道它删除的时候会去获取自定义Realm中凭证信息,如图所示:

在这里插入图片描述
既然这样的话我就可以重写getAvailablePrincipal()方法,保证删除的时候和登录的凭证信息保持一致就行,示例代码如下:

@Component
public class UserRealm extends AuthorizingRealm {// 省略其它代码... ...@Overrideprotected Object getAvailablePrincipal(PrincipalCollection principals) {User availablePrincipal = (User) super.getAvailablePrincipal(principals);return availablePrincipal.getUsername();}
}

至此退出登录时遇到的所有问题基本都解决了。

不清除缓存基本上就是key不匹配导致的问题,然后再清除过程中碰到的异常错误也都进行了解答。

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

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

相关文章

2024国城杯 Web

这四道题目Jasper大佬都做了镜像可以直接拉取进行复现 https://jaspersec.top/2024/12/16/0x12%20%E5%9B%BD%E5%9F%8E%E6%9D%AF2024%20writeup%20with%20docker/ n0ob_un4er 这道题没有复现成功, 不知道为啥上传了文件, 也在 /tmp目录下生成了sess_PHPSESSID的文件, 但是就是…

el-input输入框需要支持多输入,最后传输给后台的字段值以逗号分割

需求&#xff1a;一个输入框字段需要支持多次输入&#xff0c;最后传输给后台的字段值以逗号分割 解决方案&#xff1a;结合了el-tag组件的动态编辑标签 那块的代码 //子组件 <template><div class"input-multiple-box" idinputMultipleBox><div>…

nginx 的 server 块配置解析

前后端分离&#xff08;前端 flask&#xff09;&#xff1a; # 阻止ip访问server {# default_server 是一个配置参数&#xff0c;用于指定当请求的域名&#xff08;Host 头&#xff09;没有匹配任何 server 块时&#xff0c;Nginx 应该使用哪个 server 块来处理这些请求。 lis…

Ubuntu 22.04.5 修改IP

Ubuntu22.04.5使用的是netplan管理网络&#xff0c;因此需要在文件夹/etc/netplan下的01-network-manager-all.yaml中修改&#xff0c;需要权限&#xff0c;使用sudo vim或者其他编辑器&#xff0c;修改后的内容如下&#xff1a; # Let NetworkManager manage all devices on …

‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件。

这个错误信息表示系统找不到 vue-cli-service 命令&#xff0c;通常是因为 Vue 项目没有正确安装所需的依赖包。解决这个问题的步骤如下&#xff1a; 1. 确保你已经安装了依赖 首先&#xff0c;确保你在项目目录下&#xff0c;并且运行了以下命令来安装项目所需的依赖&#x…

解决virtualbox克隆ubuntu虚拟机之后IP重复的问题

找遍了国内论坛&#xff0c;没一个能解决该问题的&#xff0c;所以我自己写个文章吧&#xff0c;真讨厌那些只会搬运的&#xff0c;污染国内论坛环境&#xff0c;搜一个问题&#xff0c;千篇一律。 问题 操作系统版本为"Ubuntu 24.04 LTS" lennytest1:~$ cat /etc…

基于SpringBoot的宠物寄养系统的设计与实现(源码+SQL+LW+部署讲解)

文章目录 摘 要1. 第1章 选题背景及研究意义1.1 选题背景1.2 研究意义1.3 论文结构安排 2. 第2章 相关开发技术2.1 前端技术2.2 后端技术2.3 数据库技术 3. 第3章 可行性及需求分析3.1 可行性分析3.2 系统需求分析 4. 第4章 系统概要设计4.1 系统功能模块设计4.2 数据库设计 5.…

idea 开发Gradle 项目

在Mac上安装完Gradle后&#xff0c;可以在IntelliJ IDEA中配置并使用Gradle进行项目构建和管理。以下是详细的配置和使用指南&#xff1a; 1. 验证Gradle是否已安装 在终端运行以下命令&#xff0c;确保Gradle安装成功&#xff1a; gradle -v如果输出Gradle版本信息&#xff…

REST与RPC的对比:从性能到扩展性的全面分析

在微服务架构中&#xff0c;服务间通信是核心问题之一。常见的两种通信方式是REST&#xff08;Representational State Transfer&#xff09;和RPC&#xff08;Remote Procedure Call&#xff09;。它们各有优缺点&#xff0c;适用于不同场景。本文将从性能、扩展性、兼容性和开…

【Linux】:线程安全 + 死锁问题

&#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;Linux—登神长阶 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f49e; &#x1f49e; &#x1f49e; 1. 线程安全和重入问题&…

Mysql超详细安装配置教程(保姆级)

目录 一、下载Mysql 二、安装Mysql 三、配置Mysql 四、连接Mysql 五、部分疑难问题 一、下载Mysql 从官网下载MySQL&#xff0c;这里我选用的是Mysql8.0.34版本 二、安装Mysql 下载完成后直接双击进行安装&#xff0c;打开后的页面如下所示&#xff1a; “Developer Defa…

WFP Listbox绑定数据后,数据变化的刷新

Listbox绑定数据通过ItemsSource来的&#xff0c;如果绑定的是普通的List<数据>&#xff0c;不会自己刷新。 使用ObservableCollection集合 解决问题的方法: 将数组替换为 ObservableCollection ObservableCollection 是专为绑定设计的集合类型&#xff0c;可以通知 W…

JVM 及内存管理:掌握 Java 8 的内存模型与垃圾回收机制

Java 虚拟机&#xff08;JVM&#xff09;是运行 Java 程序的核心&#xff0c;它负责代码执行和内存管理。Java 8 引入了一些重要的内存模型和垃圾回收机制优化。本文将详细解析 JVM 的内存模型、垃圾回收机制&#xff0c;并配以相关图解&#xff0c;帮助你深刻理解 JVM 的工作原…

Maple软件的安装和使用

文章目录 1.前言说明2.我为什么要学习Maple3.软件的安装4.如何使用4.1基本的赋值语句4.2函数的定义4.3三个类型的书写介质 5.指数运算5.1使用面板5.2自己输入 6.对数的使用 1.前言说明 众所周知&#xff0c;我虽然是一名这个计算机专业的学生&#xff0c;但是我对于数学&#…

【超级详细】Vue3项目上传文件到七牛云的详细笔记

概述 继上一篇笔记介绍如何绑定七牛云的域名之后&#xff0c;这篇笔记主要介绍了如何在Vue3项目中实现文件上传至七牛云的功能。我们将使用Cropper.js来处理图像裁剪&#xff0c;并通过自定义组件和API调用来完成整个流程。 这里直接给出关键部分js代码&#xff0c;上传之前要先…

Sqoop的使用

每个人的生活都是一个世界&#xff0c;即使最平凡的人也要为他那个世界的存在而战斗。 ——《平凡的世界》 目录 一、sqoop简介 1.1 导入流程 1.2 导出流程 二、使用sqoop 2.1 sqoop的常用参数 2.2 连接参数列表 2.3 操作hive表参数 2.4 其它参数 三、sqoop应用 - 导入…

FFmpeg 4.3 音视频-多路H265监控录放C++开发二十一.4,SDP协议分析

SDP在4566 中有详细描述。 SDP 全称是 Session Description Protocol&#xff0c; 翻译过来就是描述会话的协议。 主要用于两个会话实体之间的媒体协商。 什么叫会话呢&#xff0c;比如一次网络电话、一次电话会议、一次视频聊天&#xff0c;这些都可以称之为一次会话。 那为什…

智简未来创新与简化的AI之路

附上链接地址&#xff1a;https://aint.top 在这个数字化迅速发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;不仅仅是技术的前沿&#xff0c;它正在成为每个行业创新的核心推动力。作为一家专注于AI技术应用与创新的公司&#xff0c;智简未来旨在通过智能化的工具…

[极客大挑战 2019]HardSQL 1

看了大佬的wp&#xff0c;没用字典爆破&#xff0c;手动试出来的&#xff0c;屏蔽了常用的关键字&#xff0c;例如&#xff1a;order select union and 最搞的是&#xff0c;空格也有&#xff0c;这个空格后面让我看了好久&#xff0c;该在哪里加括号。 先传入1’ 1试试&#…

【Pytorch实用教程】深入了解 torchvision.models.resnet18 新旧版本的区别

深入了解 torchvision.models.resnet18 新旧版本的区别 在深度学习模型开发中,PyTorch 和 torchvision 一直是我们不可或缺的工具。近期,torchvision 对其模型加载 API 进行了更新,将旧版的 pretrained 参数替换为新的 weights 参数。本文将介绍这一变化的背景、具体区别,…