spring security 關於 http.sessionManagement().maximumSessions(1);的探究


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
//真想哭
View Code

其他的這里沒必要再寫一次了

不過前端文件我需要特別改一下

 

 我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擠下線了 !!!!

    測試成功,撒花!!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM