一、记住我功能基本原理
原理图
一个请求,先进入UsernamePasswordAuthenticationFilter,当这个过滤器认证成功之后,会调用一个RemeberMeService服务,在RemeberMeService类里面有一个TokenRepository方法。
RemeberMeService这个服务会干什么呢?它会生成一个token,然后将这个token存入到浏览器的Cookie中去。同时TokenRepository方法还可以将这个Token写入到数据库中,因为我这个动作是在通过UsernamePasswordAuthenticationFilter认证成功之后去做的,所以在存入DB的时候会将用户名和token存入进去即token和用户名是一一对应的。
等第二天这个同一个用户再次访问系统的时候,这个请求在经过过滤器链的时候会经过RememberMeAuthenticationFilter过滤器,这个过滤器的作用就是读取cookie中的token,然后交给RemeberMeService,RemeberMeService会用TokenRepository到数据库中去查询这个token在数据库中有没有记录,如果有记录会将username取出来,取出来之后会调用UserDetailsService去获取用户信息,然后将用户信息存入到SecurityContext中去,以此来实现记住我功能。
RemeberMeService的过滤器链位置
二、记住我功能具体实现
1.页面处理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>标准登录页面</h1>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td>验证码:</td>
<td>
<input type="text" name="imageCode"/>
<img src="/code/image?width=150">
</td>
</tr>
<tr>
<td colspan="2"><input name="remember-me" type="checkbox" value="true"/>记住我</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
2.配置数据库
因为添加"记住我"这个功能需要用到DB,所以我在properties文件中去加入我的数据库的信息
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tinner-demo?useUnicode=yes
spring.datasource.username=root
spring.datasource.password=root
3.配置BrowserSecurityConfig
首先去配置PersistentTokenRepository ,在这个里面将dataSource注入进去,然后在这个类中有一个tokenRepository.setCreateTableOnStartup(true);方法,这个方法会去db中新建一个存储token和用户名的表,然后项目其中之后,会自动地去DB中新建这个表,但是在之后就不能再把这个打开了,需要注释掉。
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
然后在BrowserProperties中去设置一个默认的记住我的时间,这个也是可以在配置文件中去配置的。默认我写了3600秒
package com.tinner.security.core.properties;
public class BrowserProperties {
private String loginPage = "/tinner-signIn.html";
private LoginType loginType = LoginType.JSON;
private int rememberMeSecond = 3600;
public int getRememberMeSecond() {
return rememberMeSecond;
}
public void setRememberMeSecond(int rememberMeSecond) {
this.rememberMeSecond = rememberMeSecond;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
然后在configure方法中配置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(tinnerAuthentivationSuccessHandler)
.failureHandler(tinnerAuthentivationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSecond())
.userDetailsService(userDetailsService)
就是要去调用rememberMe服务,然后调用tokenRepository方法, 设置一个cookie有效时间,最后返回回去的时候调用userDetailsService这个服务,将用户信息返回给前台。
三、记住我功能SpringSecurity源码解析
1.登录解析
首先,请求会进入UsernamePasswordAuthenticationFilter中,校验完用户名密码之后会调用rememberMeService的loginSuccess方法
在这个登录成功方法里面,它做了两件事,第一件事是用tokenRepository去创建一个新的token存入到数据库中。第二件是就是将生成的token存入到浏览器的cookie中去。
2.在有remeberMe功能开启的情况下再次登录解析
现在浏览器中输入url,然后请求进入到了RememberMeAuthenticationFilter过滤器中,它首先会判断SecurityContextHolder中是不是有一个认证过的Authentication,如果没有就会去调用rememberMeService的autoLogin方法
在这个方法中首先会从请求的cookie中拿到token,然后再调用getTokenForService去数据库中去拿相应的token和用户名信息。如果token没值就会抛出异常,如果有值则会去进行判断(token是否过期等)
这些检查都通过了,最终会调用getUserDetailsService的loadUserByUsername方法。
即用找到的那个用户名去调用UserDetailService去获取用户信息,最终返回到RememberMeAuthenticationFilter。
然后RememberMeAuthenticationFilter拿到了之后会将用户信息存入到session中去。