1.前言
spring security 支持對session的管理 ,
http.sessionManagement().maximumSessions(1);的意思的開啟session管理,session並發最多一個,超出后,
舊的session被注銷,新的會注冊,這種操作稱為缺省實現 。
session缺省實現原理是將session記錄在內存map中,因此不能用於集群環境中,會導致服務器1中記錄的信息和服務器2記錄的信息並不相同;
解決的方案是使用spring session ,session存在redis里面作為共享信息【具體以后的隨筆會詳細講解,這里不多解釋】
2.操作
使用上一隨筆做的spring security前后端分離跨域的工程做測試
security完整配置

package com.example.securityqh5601.config.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; //這個加不加無所謂 //@Configuration //開啟security自定義配置 @EnableWebSecurity //開啟 Controller層的訪問方法權限,與注解@PreAuthorize("hasRole('ROLE_admin')")配合,會攔截注解了@PreAuthrize注解的配置 // 想要@PreAuthorize正確執行 ,權限關鍵字必須帶前綴 ROLE_ ,后面的部分可以隨便寫!!!!靠,琢磨了4小時了 ,終於找到原因了 @EnableGlobalMethodSecurity(prePostEnabled = true) // public class WebSecurityConfig2 extends WebSecurityConfigurerAdapter { //實例自定義登錄校驗接口 【內部有 數據庫查詢】 @Autowired private DbUserDetailsService2 dbUserDetailsService; /** * 忽略過濾的靜態文件路徑 */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers( "/js/**/*.js", "/css/**/*.css", "/img/**", "/html/**/*.html" ); } /** * 全局的跨域配置 */ @Bean public WebMvcConfigurer WebMvcConfigurer() { return new WebMvcConfigurer() { public void addCorsMappings(CorsRegistry corsRegistry) { //僅僅讓/login可以跨域 corsRegistry.addMapping("/login").allowCredentials(true).allowedHeaders("*"); //僅僅讓/logout可以跨域 corsRegistry.addMapping("/logout").allowCredentials(true).allowedHeaders("*"); //允許所有接口可以跨域訪問 //corsRegistry.addMapping("/**").allowCredentials(true).allowedHeaders("*"); } }; } //攔截規則設置 @Override protected void configure(HttpSecurity http) throws Exception { //開啟授權認證 //開啟跨域共享,關閉同源策略【允許跨域】 http.cors() //跨域偽造請求=無效, .and().csrf().disable(); //配置路徑攔截規則 http.authorizeRequests() // 只要有 "user","admin"任意最少一個權限即可訪問路徑"/user/**"的所有接口 // .antMatchers("/user/**").hasAnyAuthority("ROLE_user", "ROLE_admin") // //只有權限"admin"才可以訪問"/admin/**"所有路徑 和 接口 "/vip" // .antMatchers("/admin/**", "/vip").hasAuthority("ROLE_admin") //所有請求都需要驗證,必須要放在antMatchers路徑攔截之后,不然攔截失效 .anyRequest().authenticated(); // //路徑攔截權限的名稱必須與權限列表注冊的一樣,經過測試,方法級別的注解權限需要ROLE_前綴 ,因此, // 路徑攔截權限的名稱、注解權限名稱、數據庫存儲的權限名稱都要加ROLE_前綴最好,避免出現錯誤, // 如果數據庫的權限名稱不加ROLE_前綴,那么在注冊權限列表的時候記得拼接ROLE_前綴 //登錄配置 http.formLogin() //登錄名參數 .usernameParameter("username") //密碼參數 .passwordParameter("password") //post登錄訪問路徑 .loginProcessingUrl("/login"); //登錄結果處理 http.formLogin() //登錄成功 .successHandler(new CustomAuthenticationSuccessHandler()) //--0 //登錄失敗 .failureHandler(new CustomAuthenticationFailureHandler()); //--0 //登錄退出處理 http.logout() ////post登出訪問路徑 .logoutUrl("/logout") //成功退出處理 .logoutSuccessHandler(new CustomLogoutSuccessHandler()) //--0 //清除認證信息 .clearAuthentication(true).permitAll(); //異常拋出處理 http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()) //-- 401 //訪問拒絕處理【無權】 .accessDeniedHandler(new CustomAccessDeniedHandler()); //--2 //開啟cookie自動登錄 http.rememberMe() //自動登錄成功處理//todo // .authenticationSuccessHandler(new ....) //密鑰 .key("unique-and-secret") //cookie名 .rememberMeCookieName("remember-me-cookie-name") //生命周期,單位毫秒 .tokenValiditySeconds(24 * 60 * 60); //session並發管理 ,原理是其缺省實現是將session記錄在內存map中,因此不能用於集群環境中,服務器1中記錄的信息和服務器2記錄的信息並不相同; // // Session的並發控制,這里設為最多一個,只允許一個用戶登錄,如果同一個賬戶兩次登錄,那么第一個賬戶將被踢下線 http.sessionManagement().maximumSessions(1); //當一個用戶已經認證過了,在另外一個地方重新進行登錄認證,spring security可以阻止其再次登錄認證,從而保持原來的會話可用性 //存在一個問題,當用戶登陸后,沒有退出直接關閉瀏覽器,則再次打開瀏覽器時,此時瀏覽器的session若被刪除的話,用戶只能等到服務器的session過期后,才能再次登錄。 // http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true); //默認是開通session fixation防護的.因此可以不寫,防護的原理為,每當用戶認證過后,就會重新生成一個新的session,並拋棄舊的session // http.sessionManagement().sessionFixation().migrateSession(); //解決不允許顯示在iframe的問題 // http.headers().frameOptions().disable(); } /** * 添加 UserDetailsService, 實現自定義登錄校驗,數據庫查詢 */ @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { //注入用戶信息,每次登錄都會來這查詢一次信息,因此不建議每次都向mysql查詢,應該使用redis builder.userDetailsService(dbUserDetailsService) //密碼加密方式 .passwordEncoder(passwordEncoder()); } /** * BCryptPasswordEncoder相關知識: * 用戶表的密碼通常使用MD5等不可逆算法加密后存儲,為防止彩虹表破解更會先使用一個特定的字符串(如域名)加密,然后再使用一個隨機的salt(鹽值)加密。 * 特定字符串是程序代碼中固定的,salt是每個密碼單獨隨機,一般給用戶表加一個字段單獨存儲,比較麻煩。 * BCrypt算法將salt隨機並混入最終加密后的密碼,驗證時也無需單獨提供之前的salt,從而無需單獨處理salt問題。 */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } //參考 博文原址 //https://www.dazhuanlan.com/2019/10/01/5d92e281abbc4/ //https://www.cnblogs.com/guos/archive/2019/10/02/11617243.html //https://blog.csdn.net/icarusliu/article/details/78722384 //真想哭
其他的這里沒必要再寫一次了
不過前端文件我需要特別改一下
我i什么這樣做呢?
因為: // 當session並發上線被踢下線時,xhr會返回信息 //{"readyState":4,"responseText":"This session has been expired // (possibly due to multiple concurrent logins being attempted // as the same user).","status":200,"statusText":"parsererror"}
3.測試
啟動工程
(1)使用兩個不同的瀏覽器分別訪問網址 ,分別設為A 和 B 好區分
賬戶=cen ,密碼 = 11
A登錄,顯示登錄成功
(2)A點擊獲取認證信息,可以獲取
(3)此時,B也登錄上面的賬戶,此時B顯示登錄成功,而A沒變化 ,那是因為沒有socket 協議,服務器無法主動向前端傳數據
(4)現在B點發送信息,成功獲取處理結果,不影響業務
(5)現在,A點擊獲取認證信息,提示 “被強制下線,已在另一台設備登錄”
顯然,B把A擠下線了 !!!!
測試成功,撒花!!!!