Spring Security技術棧開發企業級認證與授權(十)開發記住我功能

「記住我」幾乎在登陸的時候都會被用戶勾選,因為它方便地幫助用戶減少了輸入用戶名和密碼的次數,本文將從三個方面介紹「記住我」功能,首先介紹「記住我」功能基本原理,然後對「記住我」功能進行實現,最後簡單解析Spring Security「記住我」功能的源碼。

一、Spring Security的記住我功能基本原理

Spring Security「記住我」功能的基本原理流程圖如下所示:

對上面的原理圖進行簡單說明:

  • 首先瀏覽器發送登錄請求,也就是認證的請求,首先會進入到UsernamePasswordAuthenticationFilter過濾器中進行驗證操作,驗證完成之後,這個過濾器還有一項額外的操作,那就是調用RememberMeService服務,這個服務中包含一個TokenRepository,它會生成一個Token,並且會將Token寫回到瀏覽器的Cookie中,並使用TokenRepository將用戶名和Token寫入到數據庫中,也就是說,用戶名和Token是一一對應的。
  • 當用戶再次請求的時候,將不會攜帶用戶名和密碼,這時候由RememberMeAuthenticationFilter讀取Cookie中的Token來進行驗證操作,這時候會使用TokenRepository從數據庫中根據Token來查詢相關信息,最後調用UserDetailsService來登錄驗證操作。
  • 這裡僅僅是簡單介紹,後面將通過打斷點的方式進入源碼進行分析。

二、Spring Security的記住我功能的實現

首先我們在瀏覽器的屬性類BrowserProperties中添加一個字段rememberMeSeconds,這個字段用來描述「記住我」的時間期限,具體的配置類代碼如下:

package com.lemon.security.core.properties;    import lombok.Data;    /**   * @author lemon   * @date 2018/4/5 下午3:08   */  @Data  public class BrowserProperties {        private String loginPage = "/login.html";        private LoginType loginType = LoginType.JSON;        private int rememberMeSeconds = 3600;  }

修改完這個類之後,它就支持用戶自定義配置時間了,這裡默認的有效期是一個小時,也就是說在一個小時內重複登錄,無需輸入用戶名和密碼。 在瀏覽器的安全配置類BrowserSecurityConfig中添加一個Bean,這個Bean就是TokenRepository,配置完這個Bean就基本完成了「記住我」功能的開發,然後在將這個Bean設置到configure方法中即可。 具體代碼如下:

@Bean  public PersistentTokenRepository tokenRepository() {      JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();      tokenRepository.setDataSource(dataSource);      tokenRepository.setCreateTableOnStartup(true);      return tokenRepository;  }

上面的代碼tokenRepository.setCreateTableOnStartup(true);是自動創建Token存到數據庫時候所需要的表,這行代碼只能運行一次,如果重新啟動數據庫,必須刪除這行代碼,否則將報錯,因為在第一次啟動的時候已經創建了表,不能重複創建。其實建議查看JdbcTokenRepositoryImpl類中的一個常量字段CREATE_TABLE_SQL,這個字段是描述了建表的一個SQL語句,建議手動複製這個SQL語句建表,那麼就完全不需要tokenRepository.setCreateTableOnStartup(true);這行代碼。完整的配置代碼如下所示:

package com.lemon.security.browser;    import com.lemon.security.core.properties.SecurityProperties;  import com.lemon.security.core.validate.code.ValidateCodeFilter;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.context.annotation.Bean;  import org.springframework.context.annotation.Configuration;  import org.springframework.security.config.annotation.web.builders.HttpSecurity;  import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  import org.springframework.security.core.userdetails.UserDetailsService;  import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  import org.springframework.security.crypto.password.PasswordEncoder;  import org.springframework.security.web.authentication.AuthenticationFailureHandler;  import org.springframework.security.web.authentication.AuthenticationSuccessHandler;  import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;  import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;    import javax.sql.DataSource;    /**   * 瀏覽器安全驗證的配置類   *   * @author lemon   * @date 2018/4/3 下午7:35   */  @Configuration  public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {        private final SecurityProperties securityProperties;      private final AuthenticationSuccessHandler lemonAuthenticationSuccessHandler;      private final AuthenticationFailureHandler lemonAuthenticationFailureHandler;      private final DataSource dataSource;        @Autowired      public BrowserSecurityConfig(SecurityProperties securityProperties, AuthenticationSuccessHandler lemonAuthenticationSuccessHandler, AuthenticationFailureHandler lemonAuthenticationFailureHandler, DataSource dataSource) {          this.securityProperties = securityProperties;          this.lemonAuthenticationSuccessHandler = lemonAuthenticationSuccessHandler;          this.lemonAuthenticationFailureHandler = lemonAuthenticationFailureHandler;          this.dataSource = dataSource;      }        @Autowired      private UserDetailsService userDetailsService;        /**       * 配置了這個Bean以後,從前端傳遞過來的密碼將被加密       *       * @return PasswordEncoder實現類對象       */      @Bean      public PasswordEncoder passwordEncoder() {          return new BCryptPasswordEncoder();      }        @Bean      public PersistentTokenRepository tokenRepository() {          JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();          tokenRepository.setDataSource(dataSource);          return tokenRepository;      }        @Override      protected void configure(HttpSecurity http) throws Exception {            ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();          validateCodeFilter.setAuthenticationFailureHandler(lemonAuthenticationFailureHandler);          validateCodeFilter.setSecurityProperties(securityProperties);          validateCodeFilter.afterPropertiesSet();            http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)                  .formLogin()                  .loginPage("/authentication/require")                  .loginProcessingUrl("/authentication/form")                  .successHandler(lemonAuthenticationSuccessHandler)                  .failureHandler(lemonAuthenticationFailureHandler)                  .and()                  .rememberMe()                  .tokenRepository(tokenRepository())                  .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())                  .userDetailsService(userDetailsService)                  .and()                  .authorizeRequests()                  .antMatchers("/authentication/require", securityProperties.getBrowser().getLoginPage(), "/code/image").permitAll()                  .anyRequest()                  .authenticated()                  .and()                  .csrf().disable();      }  }

注意上面的代碼,重新注入了DataSourceUserDetailsService,其中UserDetailsService並沒有使用構造器注入,而是字段注入,這是因為UserDetailsService的實現類中注入了PasswordEncoderBean,這就造成了依賴注入的循環應用問題。 配置完這麼多,基本完成了「記住我」的功能,最後還需要在登錄頁面添加一個checkbox,如下所示:

<tr>      <td colspan="2"><input name="remember-me" type="checkbox" value="true">記住我</td>  </tr>

其中name屬性必須是remember-me

這時候啟動項目,在登錄頁面勾選「記住我」複選框,然後登錄,登錄完成之後,關閉項目,再次啟動項目,嘗試訪問一個服務,這時候是直接可以訪問的,而不需要重新登錄。

三、Spring Security的記住我功能源碼解析

這裡再次展示Spring Security「記住我」功能的原理圖,根據這個圖以及源碼來進行解析。

1)第一次登錄

  • 第一步:當用戶發送登錄請求的時候,首先到達的是UsernamePasswordAuthenticationFilter這個過濾器,然後執行attemptAuthentication方法的代碼,代碼如下圖所示:
  • 第二步:驗證成功之後,將進入AbstractAuthenticationProcessingFilter類的successfulAuthentication的方法中,首先將認證信息通過代碼SecurityContextHolder.getContext().setAuthentication(authResult);將認證信息存入到session中,緊接着這個方法中就調用了rememberMeServices的loginSuccess方法,如下圖所示:
  • 第三步:進入rememberMeServicesloginSuccess方法中,可以看出,它方法內部調用了PersistentTokenBasedRememberMeServicesonLoginSuccess方法,代碼如下:

這個方法中調用了tokenRepository來創建Token並存到數據庫中,且將Token寫回到了Cookie中。到這裡,基本的登錄過程基本完成,生成了Token存到了數據庫,且寫回到了Cookie中。

2)第二次再次訪問 重啟項目,這時候服務器端的session已經不存在了,但是第一次登錄成功已經將Token寫到了數據庫和Cookie中,直接訪問一個服務,嘗試不輸入用戶名和密碼,看看接下來都經歷了一些什麼。

  • 第一步:首先進入到了RememberMeAuthenticationFilterdoFilter方法中,這個方法首先檢查在session中是否存在已經驗證過的Authentication了,如果為空,就進行下面的RememberMe的驗證代碼,比如調用rememberMeServicesautoLogin方法,代碼如下:
  • 第二步:然後進入PersistentTokenBasedRememberMeServiceprocessAutoLoginCookie方法中,從請求中的Cookie中拿到Token,並且調用tokenRepositorygetTokenForSeries從數據庫中查詢到Token,接下來就是進行一系列的對比驗證工作。最後調用UserDetailsService來完成返回UserDetails的實現類對象。
  • 第三步:再次返回到RememberMeAuthenticationFilter中將登錄信息存儲到session中,然後去訪問自定義的RESTful API。這就完成了整個功能的源碼解析。

Spring Security技術棧開發企業級認證與授權系列文章列表:

Spring Security技術棧開發企業級認證與授權(一)環境搭建 Spring Security技術棧開發企業級認證與授權(二)使用Spring MVC開發RESTful API Spring Security技術棧開發企業級認證與授權(三)表單校驗以及自定義校驗註解開發 Spring Security技術棧開發企業級認證與授權(四)RESTful API服務異常處理 Spring Security技術棧開發企業級認證與授權(五)使用Filter、Interceptor和AOP攔截REST服務 Spring Security技術棧開發企業級認證與授權(六)使用REST方式處理文件服務 Spring Security技術棧開發企業級認證與授權(七)使用Swagger自動生成API文檔 Spring Security技術棧開發企業級認證與授權(八)Spring Security的基本運行原理與個性化登錄實現 Spring Security技術棧開發企業級認證與授權(九)開發圖形驗證碼接口 Spring Security技術棧開發企業級認證與授權(十)開發記住我功能 Spring Security技術棧開發企業級認證與授權(十一)開發短訊驗證碼登錄 Spring Security技術棧開發企業級認證與授權(十二)將短訊驗證碼驗證方式集成到Spring Security Spring Security技術棧開發企業級認證與授權(十三)Spring Social集成第三方登錄驗證開發流程介紹 Spring Security技術棧開發企業級認證與授權(十四)使用Spring Social集成QQ登錄驗證方式 Spring Security技術棧開發企業級認證與授權(十五)解決Spring Social集成QQ登錄後的註冊問題 Spring Security技術棧開發企業級認證與授權(十六)使用Spring Social集成微信登錄驗證方式

示例代碼下載地址:

項目已經上傳到碼雲,歡迎下載,內容所在文件夾為chapter010