This chapter will continue to expand the function and come to the realization of a "remember me" function, which means that after the user logs in once, the system will remember the user for a period of time. During this time, the user can use the system without logging in again.
Remember Me Basic Principles of Function
Principle explanation
- When a user logs in and sends an authentication request, it will be
UsernamePasswordAuthenticationFilter
intercepted by the authentication. After the authentication is successful, aRememberMeService
service will be called . There is oneTokenRepository
in the service. This service will generate a Token, and then write the Token into the cookie of the browser and use the TokenRepository to convert the generated Token. Write to the database , because this action is done after the authentication is successful, so when the Token is written into the database, the user name will be written into the database at the same time. - If the browser is closed to re-access the system, the user can access without logging in again. At this time, the request will pass through
RememberMeAuthenticationFilter
the filter chain. The function of this filter is to read the Token in the Cookie and give it to RemeberMeService, and RemeberMeService will use TokenRepository to Check the database to check whether the Token has a record in the database. If there is a record, the user name will be taken out. After it is taken out, various verifications will be performed and a new Token will be generated before calling the previous oneUserDetailService
to obtain the user’s information, and then put the userSecurityContext
Put the information in it, and log in the user here.
Illustrated
Where is RememberMeAuthenticationFilter in the filter chain?
- Diagram
- First, other authentication filters will authenticate first, and when other filters are unable to authenticate,
RememberMeAuthenticationFilter
they will try to authenticate.
Remember me function concrete realization
Front page
- When logging in, add a line to remember my check button. Note here that the name must be
remember-me
, which will be mentioned in the source code section below.
<tr>
<td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
</tr>
Backstage
- First configure the
TokenRepository
Bean
/**
* 记住我功能的Token存取器配置
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
- Then you need to configure all the components of the remember me function in the configure configuration method.
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(meicloudAuthenticationSuccessHandler)
.failureHandler(meicloudAuthenticationFailureHandler)
// 配置记住我功能
.and()
.rememberMe()
// 配置TokenRepository
.tokenRepository(persistentTokenRepository())
// 配置Token过期时间
.tokenValiditySeconds(3600)
// 最终拿到用户名之后,使用UserDetailsService去做登录
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/image").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
Remember me feature Spring Security source code analysis
"Remember me" source code flow before login
- After successful authentication, calls successfulAuthentication method (Chapter V source part of these have already learned), the authentication information is saved to the Context after
RememberMeServices就会调用它的loginSuccess方法
.
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
- The loginSuccess method will first check whether there is a name
remember-me
parameter in the request before proceeding to the next step.
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
// this.parameter = "remember-me"
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
- Then enter the onLoginSuccess method, which is mainly
写库和写Cookie
the operation.
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
// 生成Token
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
// 将Token和userName插入数据库
this.tokenRepository.createNewToken(persistentToken);
// 将Token写到Cookie中
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
"Remember me" source code flow after login
- First, it will enter
RememberMeAuthenticationFilter
, and it will first determine whether the previous filter has been authenticated (whether there is authentication information in the Context). If it has not been authenticated, it will call the autoLogin method of RememberMeServices.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
} catch (AuthenticationException var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
}
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var8);
}
}
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
- In the autoLogin method,
this.processAutoLoginCookie(cookieTokens, request, response)
this method is mainly called to obtain user information in the database. The steps are: - Parse the cookie from the front end, which contains Token and seriesId, it will
使用seriesId查找数据库的Token
- Check the Token in the Cookie and the Token found in the database
是否一样
- If the same, then check the Token in the database
是否已过期
- If all the above are met, the old user name and series will be used to renew a new Token. At this time
过期时间也重新刷新
- Then the new Token saved back
数据库
, but added backCookie
in - Finally, call the loadUserByUsername method of UserDetailsService
返回UserDetails
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}
- Back to RememberMeAuthenticationFilter, after calling the autoLogin method, rememberMeAuth is obtained, and then an authentication is performed on it. After the authentication is successful
保存到SecurityContext
, the source code of the entire RememberMe automatic login process ends.