登录页面一般都会有记住我(或者是保持登录)这样的一个选项,用于在某段时间范围内退出浏览器不用再重新登录,仍旧保持登录的状态。
1. pom.xml
增加mybatis和mysql依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2. application.yml
# 数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root123
3. login.html
增加记住我复选框,注意name必须为"remember-me"
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>登录</title>
</head>
<body>
<form method="post" action="/login">
<h2 class="form-signin-heading">登录</h2>
<span th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
<p>
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autofocus>
</p>
<p>
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</p>
<input type="checkbox" name="remember-me"/>记住我<br>
<button type="submit">登录</button>
</form>
</body>
</html>
4. SecurityConfiguration
增加rememberMe配置:
- tokenRepository 令牌仓库
- tokenValiditySeconds token保存时间,单位秒
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
// 配置需要认证的请求
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
// 登录表单相关配置
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(myAuthenticationSuccessHandler)
.failureUrl("/login?error")
.permitAll()
.and()
.rememberMe()
.userDetailsService(myUserDetailsService)
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60 * 60 * 60 * 30)
.and()
// 登出相关配置
.logout()
.permitAll();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
}
5. 创建表persistent_logins
此表Spring Security中会使用到,用于持久化用户登录的信息。
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
);
6. 测试
7. 源码分析
过程一
| 用户输入用户名密码并选中"记住我",点击登录
| UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter#doFilter
| successfulAuthentication(request, response, chain, authResult)
| AbstractRememberMeServices#loginSuccess(request, response, authResult)
| onLoginSuccess(request, response, successfulAuthentication)
| PersistentTokenBasedRememberMeServices#onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication)
| PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date())
| tokenRepository.createNewToken(persistentToken)
| JdbcTokenRepositoryImpl#createNewToken(persistentToken)
| public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"
| private String insertTokenSql = DEF_INSERT_TOKEN_SQL
| getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate())
| addCookie(persistentToken, request, response)
| AbstractRememberMeServices#setCookie(new String[] { token.getSeries(), token.getTokenValue() }, getTokenValiditySeconds(), request, response)
| response.addCookie(cookie)
- 用户输入用户名密码并选中"记住我",点击登录
- 登录被UsernamePasswordAuthenticationFilter过滤器拦截,进行认证,认证成功(successfulAuthentication)后会调用AbstractRememberMeServices#loginSuccess()
- AbstractRememberMeServices#loginSuccess()会调用PersistentTokenBasedRememberMeServices#onLoginSuccess()
- onLoginSuccess()方法首先会创建PersistentRememberMeToken对象,会生成series、token: generateSeriesData()、generateTokenData()
- 接下来调用JdbcTokenRepositoryImpl#createNewToken(persistentToken)像persistent_logins表插入一条数据
- 接下来调用addCookie(persistentToken, request, response)像reponse对象中添加Cookie, response.addCookie(cookie)
过程二
| 关闭服务器再重新启动,然后访问任意一个接口
| RememberMeAuthenticationFilter#doFilter
| Authentication rememberMeAuth = AbstractRememberMeServices#autoLogin(request, response)
| String rememberMeCookie = extractRememberMeCookie(request)
| String[] cookieTokens = decodeCookie(rememberMeCookie)
| UserDetails user = PersistentTokenBasedRememberMeServices#processAutoLoginCookie(cookieTokens, request, response)
| final String presentedSeries = cookieTokens[0]
| PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries)
| PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date())
| tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
| addCookie(newToken, request, response)
| getUserDetailsService().loadUserByUsername(token.getUsername())
| AbstractRememberMeServices#createSuccessfulAuthentication(request, user)
| RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, user, authoritiesMapper.mapAuthorities(user.getAuthorities()));
| public RememberMeAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities) {
this.keyHash = key.hashCode();
setAuthenticated(true);
}
| auth.setDetails(authenticationDetailsSource.buildDetails(request));
| rememberMeAuth = ProviderManager#authenticate(rememberMeAuth)
| RememberMeAuthenticationProvider#authenticate(authentication)
| if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(messages.getMessage("RememberMeAuthenticationProvider.incorrectKey", "The presented RememberMeAuthenticationToken does not contain the expected key"));
}
访问任意一个请求都会被RememberMeAuthenticationFilter过滤器拦截,通过remember-me Cookie可以解析出series和username字段,通过series去更新这条记录,通过用户名获取用户信息,然后创建成功认证的token(RememberMeAuthenticationToken), 最终调RememberMeAuthenticationProvider#authenticate(authentication)来认证,记住我认证逻辑只判断key是否一致,如果一致就认证通过,如果不一致就抛异常。记住我认证和用户名密码认证完全不一样,记住我认证只判断key是否一致,不会判断用户名和密码是否正确。