引言

我们在访问很多网站的时候,会看到很长一段时间可以不用登录,那这个功能其实就是记住我功能,这篇文章将使用SpringSecurity完成记住我功能,并且学习其中原理,从而达到不通过SpringSecurity也能够自己实现记住我的功能。

记住我功能基本原理

流程图

20200215104402

  • 在用户登陆的时候,也就是发送认证请求的时候是先到我们的UsernamePasswordAuthenticationFilter的
  • 当它认证成功之后会调用RememberMeService这样的一个服务,这个服务会生成一个Token,并写入浏览器的Cookie中,同时使用TokenRepository将该Token写入数据库中(带用户名)
  • 当过了一段时间后(Token失效之前),用户再访问我们的系统时,就不需要再登陆了,直接可以访问我们的服务。这个请求再经过过滤器链时,会经过一个叫RememberMeAuthenticationFilter这样的一个过滤器,这个过滤器的作用就是读取Cookie中的Token并将之交给RememberMeService,RememberMeService会使用TokenRepository到数据库中去查该Token是否在数据库中有记录。
  • 若有记录,则将该记录的用户名取出来,然后去调用我们之前提到过的UserDetailsService,去获取用户的信息,并把当前获取到的用户信息放入SecurityContext,这样就把用户登陆上了。

过滤器链

20200215105825
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这个属性配置类吧
20200215142805

5)效果检验
20200215141831
第一次启动项目的时候,第一次登陆可以看到在数据库存储了一条记录;因为已经登陆的用户的信息会被存储在Session中,所以把项目重启一次(因为表已经存在了,所以需要把tokenRepository.setCreateTableOnStartup设置为False,否则会报表已存在的异常),可以观察到在Token未过期之前,该用户再访问其他的服务时,都不需要在进行登陆了。

记住我功能流程分析

第一次发起请求时,需要身份认证,点击记住我,登陆

1)一开始进入的还是我们的UsernamePasswordAuthenticationFilter
20200215143930

2)用户名密码正确后,就进入到了我们的AbstractAuthenticationProcessingFilter的successfulAuthentication方法中
20200215144243

3)我们点进loginSuccess方法,进入的是一个AbstractRememberService的一个子类PersistentTokenBasedRememberMeServices
20200215144841
至此,登陆并记住我已经完成,我们把服务重启一下,清空Session。

第二次发起请求,访问我们的服务

1)会进入到RememberMeAuthenticationFilter这个过滤器中,它首先会判断SecurityContextHolder中是否有已认证过的Authentication,如果为空,即前面的过滤器没有进行身份认证,那么它就会调用rememberService的autoLogin方法.
20200215154310

2)进入autoLogin方法,该方法是一个AbstractRememberService的一个子类PersistentTokenBasedRememberMeServices提供的具体实现
20200215154740
20200215155023
20200215155221

4)返回用户信息后,又回到了我们的RememberMeAuthenticationFilter这个过滤器中,上面看似步骤多,其实就是我们在这个过滤器中调用rememberSerivce的autoLogin方法,将获取到的用户信息进行认证后得到一个经过认证的Authentication,再存入Session中。(认证流程参见Spring Security认证流程分析
20200215162644

Finally

🐻Stay Hungry,Stay Foolish