基於用戶名、密碼的"記住我"功能
SpringBoot 2.2.0.RELEASE
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
"記住我"功能的流程是:
- 輸入驗證信息,如用戶名、密碼、驗證碼等
- 選中 “記住我”,系統後臺根據登陸成功的用戶信息 生成token信息 放到cookie和數據庫表中
- 【系統重新啓動或退出系統重新訪問】,在cookie的過期時間內 重新登錄時 都不需要輸入用戶名、密碼,可以直接訪問請求資源。
首先,需要明確的是"記住我"是SpringSecurity默認帶的,過濾器和認證類分別是RememberMeAuthenticationFilter
和RememberMeAuthenticationProvider
我們只需要配置即可
需要注意的是,使用記住我功能,前端頁面必須要有一個name=remember-me的checkbox,
因爲SpringSecurity中與"記住我"有關的功能,將remember-me作爲參數名來進行業務處理,如AbstractRememberMeServices#rememberMeRequested()
<input name="remember-me" type="checkbox" value="true"/>記住我</td>
講到"記住我"功能,還要從之前的`UserNamePasswordAuthenticationFilter`說起,當執行了`attemptAuthentication`之後,會執行``successfulAuthentication(request, response, chain, authResult);``
有代碼爲證 AbstractAuthenticationProcessFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
//調用UserNamePasswordAuthenticationFilter方法
authResult = attemptAuthentication(request, response);
//此處省略一些代碼
successfulAuthentication(request, response, chain, authResult);
}
着重看一下successfulAuthentication方法
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//將驗證過後的信息 存到SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//調用成功處理器
successHandler.onAuthenticationSuccess(request, response, authResult);
}
會發現,該方法除了將認證信息放到SecurityContext以外,還調用了rememberMeServices.loginSuccess方法
其中
rememberMeServices.loginSuccess
的調用過程如下:
AbstractRememberMeServices#loginSuccess
==>PersistentTokenBasedRememberMeServices#onLoginSuccess
AbstractRememberMeServices
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
//這裏就是上面說的檢驗request中是否含有remember-me參數,
//如果有 才往下執行
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
可以從上面看到 parameter的默認是指remember-me。如果沒有選中‘記住我’複選框 則終止執行。
PersistentTokenBasedRememberMeServices
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
這個方法的主要含義:
從登陸成功的信息中拿到用戶名,隨後生成一條插入語句 將token等信息插入到persistent_logins
這個表中。並將token信息添加到cookie中。
persistent_logins這個是SpringSecurity默認提供出來針對“記住我”功能來記錄token和cookie的數據表
tokenRepository這個在該類中的默認實現是
new InMemoryTokenRepositoryImpl()
,我們可以在Security配置中指定我們的tokenRepository。
PersistentTokenRepository有兩種實現,分別是InMemoryTokenRepositoryImpl和JdbcTokenRepositoryImpl。兩者的區別不言而喻,一個是將token信息放到內存中,一個是將其持久化到數據庫
說了這麼多,我們來配置下
protected void configure(HttpSecurity http) throws Exception {
// ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter(failureHandler);
//表單登錄 任何請求均進行認證
http.formLogin()
//默認登錄請求是/login,這裏重置爲/authentication/form 並不影響流程
.loginProcessingUrl("/authentication/form")
.successHandler(successHandler)
.failureHandler(failureHandler)
.and()
.rememberMe()
.userDetailsService(userDetailsService)
.tokenRepository(persistentTokenRepository()).
and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf()
.disable();
}
@Autowired
private DataSource dataSource;
//這個Bean 就是上文我們說的替代默認tokenRepository的配置
@Bean
@ConditionalOnMissingBean(PersistentTokenRepository.class)
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//這句話 只有在第一次啓動時 才放開註釋 因爲會創建表,再次運行時 註釋,因爲存在的表不能被再次創建
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
關於數據源的配置
spring.datasource.url=jdbc:mysql://localhost:3306/study
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
系統運行起來後:
點擊登錄,認證成功處理器關於自定義認證處理器,請參考這篇文章會將用戶信息打印到頁面
前面我們分析過,由於選中了"remember-me",用戶名、密碼登錄成功後,會把用戶的token存到表裏 並且寫入到cookie裏一份。
我們去persistent_logins
表中看下,發現token計入了表中
重啓系統,發現不需要登錄,直接可以請求接口,這說明了我們成功了。
源碼分析
退出系統,重新再次訪問系統資源時,會先進入到RememberMeAutnenticationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//系統重新啓動或用戶退出過,所以SecurityContextHolder中沒有用戶的認證信息
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
try {
//根據自動登錄獲取到的用戶信息 進行認證 這裏認證就很簡單了 判斷hashcode是否一致
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
//將用戶認證信息重新放到SecurityContextHolder中
//SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
...
chain.doFilter(request, response);
}
}
rememberServices#autoLogin方法如下
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
//從request中獲取cookie
String rememberMeCookie = extractRememberMeCookie(request);
...
user = processAutoLoginCookie(cookieTokens, request, response);
...
//封裝用戶認證信息 這裏省略了
}
processAutoLoginCookie方法
PersistentTokenBasedRememberMeServices
UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) {
//存到數據庫中的信息如下
// 前文中提到的`persistent_logins`表中series、token 分別代表下面的兩個參數
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
//從表中取出token數據
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
...
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
//更新表中的token
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
//重新將新token 添加到cookie中
addCookie(newToken, request, response);
}
...
//獲取user信息,從這裏可以看出UserDetailsService或自定義UserDetailsService必須在RememberMe配置中配置
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
RememberMeAuthenticationProvider的認證過程相較於其他provider,比較簡單 這裏一筆帶過了
以上,是個人對”記住我“功能的個人實踐與源碼理解。如有問題,請指出,謝謝。