解決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不匹配導致的問題,然后再清除過程中碰到的異常錯誤也都進行了解答。