解決Springboot整合Shiro自定義SessionDAO+Redis管理會話,登錄后不跳轉首頁
- 問題發現
- 問題解決
問題發現
在Shiro框架中,SessionDAO的默認實現是MemorySessionDAO。它內部維護了一個ConcurrentMap來保存session數據,即將session數據緩存在內存中。
再使用Redis作為Session存儲解決分布式系統中的Session共享問題。
依賴文件如下:
<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 {@GetMapping("/index")public ModelAndView index() {Subject subject = SecurityUtils.getSubject();System.out.println("===============index==========");System.out.println(subject.getSession().getId());System.out.println(subject.isAuthenticated());if (subject.isAuthenticated() || subject.isRemembered()) {return new ModelAndView("redirect:main");}return new ModelAndView("login.html");}@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("/main")public String main() {Subject subject = SecurityUtils.getSubject();System.out.println("===============main==========");System.out.println(subject.getSession().getId());System.out.println(subject.isAuthenticated());return "main.html";}
}
自定義SessionDAO,示例代碼如下:
public class RedisSessionDao extends AbstractSessionDAO {private HashOperations<String, Object, Session> hashOperations;private static final String key = "shiro:";public RedisSessionDao(RedisTemplate<String, Object> redisTemplate) {hashOperations = redisTemplate.opsForHash();}@Overrideprotected Serializable doCreate(Session session) {Serializable sessionId = super.generateSessionId(session);this.assignSessionId(session, sessionId);this.storeSession(sessionId, session);return sessionId;}@Overrideprotected Session doReadSession(Serializable serializable) {return (Session) hashOperations.get(key, serializable.toString());}@Overridepublic void update(Session session) throws UnknownSessionException {this.storeSession(session.getId(), session);}@Overridepublic void delete(Session session) {if (session == null) {throw new NullPointerException("session argument cannot be null.");} else {Serializable id = session.getId();if (id != null) {hashOperations.delete(key, id.toString());}}}@Overridepublic Collection<Session> getActiveSessions() {return hashOperations.values(key);}protected void storeSession(Serializable id, Session session) {if (id == null) {throw new NullPointerException("id argument cannot be null.");} else {this.hashOperations.putIfAbsent(key, id.toString(), session);}}
}
Config配置文件示例代碼如下:
@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地址。Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();filterChainDefinitionMap.put("/user/login", "anon");filterChainDefinitionMap.put("/**", "authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*** 創建Shiro Web應用的整體安全管理*/@Beanpublic DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm());defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注冊會話管理// 可以添加其他配置,如緩存管理器、會話管理器等return defaultWebSecurityManager;}/*** 創建會話管理*/@Beanpublic DefaultWebSessionManager defaultWebSessionManager() {DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();defaultWebSessionManager.setGlobalSessionTimeout(10000);defaultWebSessionManager.setSessionDAO(sessionDAO());defaultWebSessionManager.setCacheManager(cacheManager());return defaultWebSessionManager;}@Beanpublic SessionDAO sessionDAO() {RedisSessionDao redisSessionDao = new RedisSessionDao(redisTemplate());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.setAuthorizationCachingEnabled(true); // 啟動授權緩存userRealm.setAuthenticationCachingEnabled(true); // 啟動驗證緩存userRealm.setCacheManager(cacheManager());return userRealm;}@Beanpublic CacheManager cacheManager() {RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());return redisCacheManage;}@Autowiredprivate RedisConnectionFactory redisConnectionFactory;// redis序列化配置@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();redisTemplate.setHashKeySerializer(stringRedisSerializer);redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());return redisTemplate;}
}
進入瀏覽器登陸成功后跳轉首頁,跳轉過程中302,返回登錄頁面,如圖所示:
問題解決
根據代碼日志,可知道,跳轉到其他頁面時Session沒有共享,如圖所示:
最開始以為Redis中沒有保存記錄,其實已經保存了,如圖所示:
參考網上諸多案例,似乎沒什么區別,也不知道他們測過沒有。
然后再Debug的時候,發現了另外一個類EnterpriseCacheSessionDAO,于是參考該類,我就把對應代碼繼承CachingSessionDAO,示例代碼如下:
public class RedisSessionDao extends CachingSessionDAO {private HashOperations<String, Object, Session> hashOperations;protected Serializable doCreate(Session session) {Serializable sessionId = this.generateSessionId(session);this.assignSessionId(session, sessionId);return sessionId;}protected Session doReadSession(Serializable sessionId) {return null;}protected void doUpdate(Session session) {}protected void doDelete(Session session) {}
}
Config配置文件,示例代碼如下:
@Beanpublic DefaultWebSessionManager defaultWebSessionManager() {DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();defaultWebSessionManager.setGlobalSessionTimeout(10000);defaultWebSessionManager.setSessionDAO(sessionDAO());defaultWebSessionManager.setCacheManager(cacheManager());return defaultWebSessionManager;}@Beanpublic SessionDAO sessionDAO() {RedisSessionDao redisSessionDao = new RedisSessionDao();redisSessionDao.setCacheManager(cacheManager()); // 設置緩存管理器redisSessionDao.setActiveSessionsCacheName("shiro:session"); // 自定義redis存放的key名稱return redisSessionDao;}
重啟項目后運行,成功跳轉,如圖所示:
Redis中也有記錄,如圖所示:
至于繼承AbstractSessionDAO為什么沒有共享Session,大概率的原因是Redis沒有被Shiro給管理導致的。
示例代碼如下:
public class RedisSessionDao extends AbstractSessionDAO {private CacheManager cacheManager;private Cache<Serializable, Session> activeSessions;private static final String key = "shiro:";public RedisSessionDao() {}public void setCacheManager(CacheManager cacheManager) {this.cacheManager = cacheManager;this.activeSessions = cacheManager.getCache(key);}@Overrideprotected Serializable doCreate(Session session) {Serializable sessionId = super.generateSessionId(session);this.assignSessionId(session, sessionId);this.storeSession(sessionId, session);return sessionId;}@Overrideprotected Session doReadSession(Serializable serializable) {return (Session) activeSessions.get(serializable);}@Overridepublic void update(Session session) throws UnknownSessionException {this.storeSession(session.getId(), session);}@Overridepublic void delete(Session session) {if (session == null) {throw new NullPointerException("session argument cannot be null.");} else {Serializable id = session.getId();if (id != null) {activeSessions.remove(id);}}}@Overridepublic Collection<Session> getActiveSessions() {return activeSessions.values();}protected void storeSession(Serializable id, Session session) {if (id == null) {throw new NullPointerException("id argument cannot be null.");} else {activeSessions.put(id, session);}}
}
配置文件,示例代碼如下:
/*** 創建會話管理*/@Beanpublic DefaultWebSessionManager defaultWebSessionManager() {DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();defaultWebSessionManager.setGlobalSessionTimeout(10000);defaultWebSessionManager.setSessionDAO(sessionDAO());defaultWebSessionManager.setCacheManager(cacheManager());return defaultWebSessionManager;}@Beanpublic SessionDAO sessionDAO() {RedisSessionDao redisSessionDao = new RedisSessionDao();redisSessionDao.setCacheManager(cacheManager()); // 設置緩存管理器return redisSessionDao;}
經過測試也是可以成功跳轉,會話共享。