引言
我们在访问很多网站的时候,会看到很长一段时间可以不用登录,那这个功能其实就是记住我功能,这篇文章将使用SpringSecurity完成记住我功能,并且学习其中原理,从而达到不通过SpringSecurity也能够自己实现记住我的功能。
记住我功能基本原理
流程图
- 在用户登陆的时候,也就是发送认证请求的时候是先到我们的UsernamePasswordAuthenticationFilter的
- 当它认证成功之后会调用RememberMeService这样的一个服务,这个服务会生成一个Token,并写入浏览器的Cookie中,同时使用TokenRepository将该Token写入数据库中(带用户名)
- 当过了一段时间后(Token失效之前),用户再访问我们的系统时,就不需要再登陆了,直接可以访问我们的服务。这个请求再经过过滤器链时,会经过一个叫RememberMeAuthenticationFilter这样的一个过滤器,这个过滤器的作用就是读取Cookie中的Token并将之交给RememberMeService,RememberMeService会使用TokenRepository到数据库中去查该Token是否在数据库中有记录。
- 若有记录,则将该记录的用户名取出来,然后去调用我们之前提到过的UserDetailsService,去获取用户的信息,并把当前获取到的用户信息放入SecurityContext,这样就把用户登陆上了。
过滤器链
RememberMeAuthenticationFilter所处的位置是在倒数第二个Filter的位置。只要前面的Filter过滤掉了(无法认证用户信息),都会经过它来判断是否需要免密登录。
记住我功能具体实现
客户端
首先是在登陆时,提供给用户一个记住我的checkbox。不过要注意的是这个checkbox的name是固定的,要写成"remember-me"
<tr>
<td colspan="2"><input name="remember-me" type="checkbox" value="true">记住我</td>
</tr>
服务端
1)首先要配置一个TokenRepository来读写数据库
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); //设置数据源
//tokenRepository.setCreateTableOnStartup(true); 在启动时创建一张表,用于存储Token相关信息,
return tokenRepository;
}
2)配置数据源以及UserDetailsService
@Autowired
private DataSource dataSource; //数据源信息在application.properties中配置即可
@Autowired
private UserDetailsService userDetailsService; //自己提供实现类
3)Security配置
//Security配置
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form") //通知过滤器去处理该路径的登陆请求
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.rememberMe() //从这里开始配置记住我
.tokenRepository(persistentTokenRepository()) //配置TokenRepository,调用上面配好的bean
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
//配置token过期时间,这里定义了一个类来专门存放properties等一些外部配置参数
.userDetailsService(userDetailsService) //配置userDetailsService
.and()
.authorizeRequests() //认证请求
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage(),
"/code/image").permitAll() //匹配器,对于部分请求放行
.anyRequest() //任何请求
.authenticated() //都需要身份认证
.and()
.csrf().disable(); //跨站请求先关闭
}
4)虽然原理看起来有点复杂,但实现起来就是10几行代码的配置,完整代码如下
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties; //可自定义参数
@Autowired //使用自定义的登陆成功处理器
private MyAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired //自定义登陆失败处理器
private MyAuthenticationFailureHandler authenticationFailureHandler;
//对密码进行加蜜
//如果是自己的加蜜方法,只需要写一个类实现PasswordEncoder,并重写encoder和match方法
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired //配置数据源,数据源信息在application.properties中配置即可
private DataSource dataSource;
@Autowired //自己提供实现类
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); //设置数据源
//tokenRepository.setCreateTableOnStartup(true); 在启动时创建一张表,用于存储Token相关信息。
return tokenRepository;
}
//Security配置
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form") //通知过滤器去处理该路径的登陆请求
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.rememberMe() //从这里开始配置记住我
.tokenRepository(persistentTokenRepository()) //配置TokenRepository,调用上面配好的bean
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
//配置token过期时间,这里定义了一个类来专门存放properties等一些外部配置参数
.userDetailsService(userDetailsService) //配置userDetailsService
.and()
.authorizeRequests() //认证请求
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage(),
"/code/image").permitAll() //匹配器,对于部分请求放行
.anyRequest() //任何请求
.authenticated() //都需要身份认证
.and()
.csrf().disable(); //跨站请求先关闭
}
}
简单的看一下securityProperties这个属性配置类吧
5)效果检验
第一次启动项目的时候,第一次登陆可以看到在数据库存储了一条记录;因为已经登陆的用户的信息会被存储在Session中,所以把项目重启一次(因为表已经存在了,所以需要把tokenRepository.setCreateTableOnStartup设置为False,否则会报表已存在的异常),可以观察到在Token未过期之前,该用户再访问其他的服务时,都不需要在进行登陆了。
记住我功能流程分析
第一次发起请求时,需要身份认证,点击记住我,登陆
1)一开始进入的还是我们的UsernamePasswordAuthenticationFilter
2)用户名密码正确后,就进入到了我们的AbstractAuthenticationProcessingFilter的successfulAuthentication方法中
3)我们点进loginSuccess方法,进入的是一个AbstractRememberService的一个子类PersistentTokenBasedRememberMeServices
至此,登陆并记住我已经完成,我们把服务重启一下,清空Session。
第二次发起请求,访问我们的服务
1)会进入到RememberMeAuthenticationFilter这个过滤器中,它首先会判断SecurityContextHolder中是否有已认证过的Authentication,如果为空,即前面的过滤器没有进行身份认证,那么它就会调用rememberService的autoLogin方法.
2)进入autoLogin方法,该方法是一个AbstractRememberService的一个子类PersistentTokenBasedRememberMeServices提供的具体实现
4)返回用户信息后,又回到了我们的RememberMeAuthenticationFilter这个过滤器中,上面看似步骤多,其实就是我们在这个过滤器中调用rememberSerivce的autoLogin方法,将获取到的用户信息进行认证后得到一个经过认证的Authentication,再存入Session中。(认证流程参见Spring Security认证流程分析)
Finally
🐻Stay Hungry,Stay Foolish