SpringSecurity整合springBoot、redis實現登錄互踢功能
背景
基於我的文章——《SpringSecurity整合springBoot、redis token動態url權限校驗》。要實現的功能是要實現一個用戶不可以同時在兩臺設備上登錄,有兩種思路:
(1)後來的登錄自動踢掉前面的登錄。
(2)如果用戶已經登錄,則不允許後來者登錄。
需要特別說明的是,項目的基礎是已經是redis維護的session。
配置redisHttpSession
設置spring session由redis 管理。
2.1去掉yml中的http session 配置,yml和註解兩者隻選其一(同時配置,隻有註解配置生效)。至於為什麼不用yml,待會提到。
2.2 webSecurityConfig中加入註解@EnableRedisHttpSession
@EnableRedisHttpSession(redisNamespace = "spring:session:myframe", maxInactiveIntervalInSeconds = 1700 , flushMode = FlushMode.ON_SAVE)
登錄後發現redis session namespace已經是我們命名的瞭
獲取redis管理的sessionRepository
我們要限制一個用戶的登錄,自然要獲取他在系統中的所有session。
2.再去查看springsSession官網的文檔。springsession官網 提供文檔https://docs.spring.io/spring-session/docs/ 2.2.2.RELEASE/reference/html5/#api-findbyindexnamesessionrepository
SessionRepository實現也可以選擇實現FindByIndexNameSessionRepository
FindByIndexNameSessionRepository提供一種方法,用於查找具有給定索引名稱和索引值的所有會話
FindByIndexNameSessionRepository實現時,可以使用方便的方法查找特定用戶的所有會話
/** * redis獲取sessionRepository * RedisIndexedSessionRepository實現 FindByIndexNameSessionRepository接口 */ @Autowired //不加@Lazy這個會報什麼循環引用... // Circular reference involving containing bean '.RedisHttpSessionConfiguration' @Lazy private FindByIndexNameSessionRepository<? extends Session> sessionRepository;
這裡註意一點,當我通過yml配置redis session是,sessionRepository下面會有紅線。
雖然不影響運行,但是強迫癥,所以改用@EnableWebSecurity註解(至於為什麼?我也不想知道…)。
將sessionRepository註入SpringSessionBackedSessionRegistry
是spring session為Spring Security提供的什麼會話並發的會話註冊表實現,大概是讓springSecurity幫我們去限制登錄,光一個sessionRepository是不行的,還得自己加點工具什麼的。
webSecurityConfig加入:
/** * 是spring session為Spring Security提供的, * 用於在集群環境下控制會話並發的會話註冊表實現 * @return */ @Bean public SpringSessionBackedSessionRegistry sessionRegistry(){ return new SpringSessionBackedSessionRegistry<>(sessionRepository); }
註:
https://blog.csdn.net/qq_34136709/article/details/106012825 這篇文章說還需要加一個HttpSessionEventPublisher來監聽session銷毀雲雲,大概是因為我用的是redis session吧,不需要這個,要瞭之後還會報錯,啥錯?我忘瞭。
新增一個session過期後的處理類
先創建一個CustomSessionInformationExpiredStrategy.java來處理session過期後如何通知前端的處理類,內容如下:
public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException { if (log.isDebugEnabled()) { log.debug("{} {}", event.getSessionInformation(), MessageConstant.SESSION_EVICT); } HttpServletResponse response = event.getResponse(); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.SESSION_EVICT, MessageConstant.SESSION_EVICT)); response.getWriter().write(responseJson); } }
註:一般都是自己重新寫返回前端的信息,不會直接用框架拋出的錯誤信息
配置到configure(HttpSecurity http)方法上
.csrf().disable() //登錄互踢 .sessionManagement() //在這裡設置session的認證策略無效 //.sessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(httpSessionConfig.sessionRegistry())) .maximumSessions(1) .sessionRegistry(sessionRegistry()) .maxSessionsPreventsLogin(false) //false表示不阻止登錄,就是新的覆蓋舊的 //session失效後要做什麼(提示前端什麼內容) .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy());
註意:https://blog.csdn.net/qq_34136709/article/details/106012825 這篇文章說session認證的原理,我看到它是執行瞭一個session的認證策略,但是我debug對應的代碼時,發現
這個session認證策略是NullAuthenticatedSessionStrategy,而不是它說的ConcurrentSessionControlAuthenticationStrategy。就是說我需要在哪裡去配置這個session 認證策略。第一時間想到瞭configure(HttpSecurity http)裡面配置
結果無效。之後看到別人的代碼,想到這個策略應該是要在登錄的時候加上去,而我們的登錄一般都需要自己重寫,自然上面的寫法會無效。於是我找到瞭自定義的登錄過濾器。
然後發現this.setSessionAuthenticationStrategy(sessionStrategy);確實存在。
public LoginFilter(UserVerifyAuthenticationProvider authenticationManager, CustomAuthenticationSuccessHandler successHandler, CustomAuthenticationFailureHandler failureHandler, SpringSessionBackedSessionRegistry springSessionBackedSessionRegistry) { //設置認證管理器(對登錄請求進行認證和授權) this.authenticationManager = authenticationManager; //設置認證成功後的處理類 this.setAuthenticationSuccessHandler(successHandler); //設置認證失敗後的處理類 this.setAuthenticationFailureHandler(failureHandler); //配置session認證策略(將springSecurity包裝redis Session作為參數傳入) ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(springSessionBackedSessionRegistry); //最多允許一個session sessionStrategy.setMaximumSessions(1); this.setSessionAuthenticationStrategy(sessionStrategy); //可以自定義登錄請求的url super.setFilterProcessesUrl("/myLogin"); }
啟動 後就發現session認證策略已經改為我們設定的策略瞭。
完整的webSecurityConfig如下:
@Configuration @EnableWebSecurity //RedisFlushMode有兩個參數:ON_SAVE(表示在response commit前刷新緩存),IMMEDIATE(表示隻要有更新,就刷新緩存) //yml和註解兩者隻選其一(同時配置,隻有註解配置生效) @EnableRedisHttpSession(redisNamespace = "spring:session:myframe", maxInactiveIntervalInSeconds = 5000 , flushMode = FlushMode.ON_SAVE) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserVerifyAuthenticationProvider authenticationManager;//認證用戶類 @Autowired private CustomAuthenticationSuccessHandler successHandler;//登錄認證成功處理類 @Autowired private CustomAuthenticationFailureHandler failureHandler;//登錄認證失敗處理類 @Autowired private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回當前URL允許訪問的角色列表 @Autowired private MyAccessDecisionManager accessDecisionManager;//除登錄登出外所有接口的權限校驗 /** * redis獲取sessionRepository * RedisIndexedSessionRepository實現 FindByIndexNameSessionRepository接口 */ @Autowired //不加@Lazy這個會報什麼循環引用... // Circular reference involving containing bean '.RedisHttpSessionConfiguration' @Lazy private FindByIndexNameSessionRepository<? extends Session> sessionRepository; /** * 是spring session為Spring Security提供的, * 用於在集群環境下控制會話並發的會話註冊表實現 * @return */ @Bean public SpringSessionBackedSessionRegistry sessionRegistry(){ return new SpringSessionBackedSessionRegistry<>(sessionRepository); } /** * 密碼加密 * @return */ @Bean @ConditionalOnMissingBean(PasswordEncoder.class) public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置 HttpSessionIdResolver Bean * 登錄之後將會在 Response Header x-auth-token 中 返回當前 sessionToken * 將token存儲在前端 每次調用的時候 Request Header x-auth-token 帶上 sessionToken */ @Bean public HttpSessionIdResolver httpSessionIdResolver() { return HeaderHttpSessionIdResolver.xAuthToken(); } /** * Swagger等靜態資源不進行攔截 */ @Override public void configure(WebSecurity web) { web.ignoring().antMatchers( "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/error", "/webjars/**", "/resources/**", "/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //配置一些不需要登錄就可以訪問的接口,這裡配置失效瞭,放到瞭securityMetadataSource裡面 //.antMatchers("/demo/**", "/about/**").permitAll() //任何尚未匹配的URL隻需要用戶進行身份驗證 .anyRequest().authenticated() //登錄後的接口權限校驗 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setAccessDecisionManager(accessDecisionManager); object.setSecurityMetadataSource(securityMetadataSource); return object; } }) .and() //配置登出處理 .logout().logoutUrl("/logout") .logoutSuccessHandler(new CustomLogoutSuccessHandler()) .clearAuthentication(true) .and() //用來解決匿名用戶訪問無權限資源時的異常 .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()) //用來解決登陸認證過的用戶訪問無權限資源時的異常 .accessDeniedHandler(new CustomAccessDeniedHandler()) .and() //配置登錄過濾器 .addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler, sessionRegistry())) .csrf().disable() //登錄互踢 .sessionManagement() //在這裡設置session的認證策略無效 //.sessionAuthenticationStrategy(new ConcurrentSessionControlAuthenticationStrategy(httpSessionConfig.sessionRegistry())) .maximumSessions(1) .sessionRegistry(sessionRegistry()) .maxSessionsPreventsLogin(false) //false表示不阻止登錄,就是新的覆蓋舊的 //session失效後要做什麼(提示前端什麼內容) .expiredSessionStrategy(new CustomSessionInformationExpiredStrategy()); //配置頭部 http.headers() .contentTypeOptions() .and() .xssProtection() .and() //禁用緩存 .cacheControl() .and() .httpStrictTransportSecurity() .and() //禁用頁面鑲嵌frame劫持安全協議 // 防止iframe 造成跨域 .frameOptions().disable(); } }
其他
@Lazy private FindByIndexNameSessionRepository<? extends Session> sessionRepository;
至於這個不加@lazy會什麼循環引用的問題,我就真的不想理會瞭。看瞭好長時間,都不知道誰和誰發生瞭循環引用。。。。。
到此這篇關於SpringSecurity整合springBoot、redis——實現登錄互踢的文章就介紹到這瞭,更多相關SpringSecurity登錄互踢內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java經典面試題匯總:Spring
- SpringBoot+SpringSession+Redis實現session共享及唯一登錄示例
- SpringSecurity OAtu2+JWT實現微服務版本的單點登錄的示例
- SpringSecurity+Redis認證過程小結
- 淺談Spring Session工作原理