Spring Security——基於表單登入認證原理及實現

午夜12點發表於2019-03-04

基於表單的登入認證

一、預設安全驗證

當我們專案裡新增spring security依賴它就已經起作用了,啟動專案訪問時,會出現彈出框。spring security預設
採用basic模式認證。瀏覽器傳送http報文請求一個受保護的資源,瀏覽器會彈出對話方塊讓輸入使用者名稱和密碼。並以用
戶名:密碼的形式base64加密,加入Http報文頭部的Authorization(預設使用者名稱為user,密碼則是會在啟動程式時後
臺console裡輸出,每次都不一樣)。後臺獲取Http報文頭部相關認證資訊,認證成功返回相應內容,失敗則繼續認證。
下面會詳細介紹具體認證流程。
複製程式碼

二、基於表單的認證原理

基本認證流程:
                     SecurityContextPersistenceFilter
                                   ↓
                   AbstractAuthenticationProcessingFilter
                                   ↓
                    UsernamePasswordAuthenticationFilter
                                   ↓
                         AuthenticationManager
                                   ↓
                         AuthenticationProvider
                                   ↓
                          userDetailsService
                                   ↓
                              userDetails
                                   ↓
                                認證通過
                                   ↓
                            SecurityContext
                                   ↓
                          SecurityContextHolder
                                   ↓
                           RememberMeServices
                                   ↓
                      AuthenticationSuccessHandler
SecurityContextPersistenceFilter會校驗請求中session是否有SecurityContext,有放SecurityContextHolder
中,返回時校驗SecurityContextHolder中是否有securityContext,有放session,從而實現認證資訊在多個請求中共
享。
AbstractAuthenticationProcessingFilter中會調自身attemptAuthentication抽象方法,
UsernamePasswordAuthenticationFilter是其一個實現類。
複製程式碼
Spring Security——基於表單登入認證原理及實現
它從請求中獲取賬號密碼後,構造了一個token,此時沒有認證,隨後會呼叫AuthenticationManager介面的
authenticate方法
複製程式碼
Spring Security——基於表單登入認證原理及實現
下面是其建構函式,我們可看到這個token沒有認證
複製程式碼

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }
複製程式碼
Spring Security——基於表單登入認證原理及實現
ProviderManager實現了AuthenticationManager介面,它獲取所有provider後遍歷呼叫其supports方法,去匹配哪
個provider支援之前申明的token,找到provider後呼叫authenticate方法。

到provider才開始認證使用者資訊,表單登入的是DaoAuthenticationProvider其父類進行以下操作:
①呼叫userDetailsService介面的loadUserByUsername方法獲取UserDetails使用者資訊
②檢查UserDetails使用者是否可用、是否賬戶沒有過期、是否賬戶沒有被鎖定
③檢查UserDetails密碼是否正確
④檢查UserDetails密碼是否沒有過期
⑤都通過會重新申明一個token,不過多了許可權集合引數如下
複製程式碼
  
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
    Collection authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
複製程式碼
此時使用者才被認證通過,上述步驟有一個錯了會丟擲AuthenticationException異常的子類,下面是其繼承圖:
複製程式碼
Spring Security——基於表單登入認證原理及實現
Spring Security——基於表單登入認證原理及實現
認證結果返回給AbstractAuthenticationProcessingFilter,將結果放到SecurityContextHolder的
SecurityContext中,若新增了記住我功能又會呼叫rememberMeServices來實現,最後呼叫
AuthenticationSuccessHandler介面成功處理。
複製程式碼

三、表單登入實現

在第二部分介紹具體原理,接下來實現自定義登入驗證。首先編寫一個config類繼承WebSecurityConfigurerAdapter
類重寫configure方法填寫配置
複製程式碼
   
 protected void configure(HttpSecurity http) throws Exception {
        http
                /**
                 * 表單登入配置
                 */
                .formLogin() //表單登入
                .loginPage("/authentication/login.html") //自定義登入頁面
                .loginProcessingUrl("/authentication/form") //與自定義登入頁面處理路徑一致
//        http.httpBasic()   basic模式彈出框登入
                .successHandler(myAuthenticationSuccessHandler)  //自定義認證成功處理
                .failureHandler(myAuthenticationFailureHandler)  //自定義認證失敗處理
                .and()
                /**
                 * 需要認證的請求配置,
                 * 注:最為具體的請求路徑放在前面,而最不具體的路徑(如anyRequest())放在最後面。
                 * 如果不這樣做的話,那不具體的路徑配置將會覆蓋掉更為具體的路徑配置
                 */
                .authorizeRequests()
                //允許這樣請求通過,需要將登入所需路徑配好,不然會一直重定向
                .antMatchers("/authentication/*").permitAll() 
                .anyRequest().authenticated()//任何請求都需要認證
                .and()
                .csrf().disable(); //關閉csrf防禦機制
    }
    複製程式碼
自定義使用者實現UserDetailsService介面重寫loadUserByUsername方法
複製程式碼

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User(username, "123456", true, true,
                true, true, new ArrayList());
    }
複製程式碼
這裡的User是security自己的,寫死了密碼為123456,正常應該根據使用者名稱從資料庫查出使用者資訊。當然密碼也不可
能為明文,這裡推薦使用BCryptPasswordEncoder加密,因為其每次加密都會生成隨機鹽加入字串中。需要在之前申
明的config類新增即可
複製程式碼

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
複製程式碼
    這四個boolean值分別表示是否可用、是否賬戶沒有過期、是否密碼沒有過期、是否賬戶沒有被鎖定。建構函式
最後一個引數為該使用者用哪些介面許可權,同樣也從資料庫查,但是這個許可權只會在登入時初始化一次,若我登入後修
改許可權,則無法同步,後續介紹解決辦法。
    接下來就是兩個登入的自定義登入處理,成功的實現AuthenticationSuccessHandler介面,失敗的則實現
AuthenticationFailureHandler介面
複製程式碼

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{
        response.setContentType("application/json;UTF-8");
        response.getWriter().println("{"success":true,"result":"" + "登入成功" + ""}");
    }
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;UTF-8");
        response.getWriter().println("{"error":true,"message":"登入名或者密碼錯誤"}");
    }
複製程式碼
    注意成功和失敗最後的方法引數是不同的,一個是成功的認證資訊,另一個失敗丟擲的認證異常json串的key可以
根據自身登入頁面ajax處理結果的屬性來定,必須保持一致。
    頁面程式碼就不復制了,做一個簡單html就行。
複製程式碼

四、Remember Me

remember me顧名思義就是記住我,在登入成功認證以後服務端會傳送cookie給瀏覽器,同時把使用者名稱、base64加密隨
機序列、生成token存入persistent_logins表中。當我下次再訪問時,會讀取cookie中的token與資料庫中的token做
對比,若一直能表示認證通過,會重新生成新的token存入資料庫中,序列戶不變,再重新生成新的cookie發給瀏覽器。
下面看具體原始碼:
複製程式碼
Spring Security——基於表單登入認證原理及實現
    在第二部分說了登入認證成功後會呼叫rememberMeServices的loginSuccess方法,這是最後呼叫的方法,我們看
到了它構造了一個rememberMeToken,將其存入資料庫,併傳送cookie。
複製程式碼
Spring Security——基於表單登入認證原理及實現
    當下次登入,會請求到RememberMeAuthenticationFilter,它呼叫rememberMeServices介面的autoLogin方法,
去對比cookie中的token與資料庫中的token,又判斷了token是否過期,驗證通過會根據token中的使用者名稱呼叫
userDetailsService的loadUserByUsername方法獲取使用者資訊。最後根據使用者資訊構造
一個RememberMeAuthenticationToken認證成功。
複製程式碼

四、Remember Me實現

remember me實現較為簡單,只需要在申明的config類中新增
複製程式碼

@Autowired
private DataSource dataSource;

@Autowired
    private UserDetailsService userDetailsService;

@Bean
    public PersistentTokenRepository persistentTokenRepository() {

        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
複製程式碼
在configure方法里加
複製程式碼
 
.rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(3600) //有效時間
                .userDetailsService(userDetailsService)
複製程式碼

五、程式碼地址

https://github.com/qumaoming/spring-security
複製程式碼

相關文章