基于用户名、密码的"记住我"功能
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,比较简单 这里一笔带过了
以上,是个人对”记住我“功能的个人实践与源码理解。如有问题,请指出,谢谢。