解決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.pswp.cn/web/64765.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/64765.shtml
英文地址,請注明出處:http://en.pswp.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 參數。本文將介紹這一變化的背景、具體區別,…