在使用Remeberme功能之后,发现甚至在可以不引入spring-session的情况下完成分布式session的功能。觉得颇为有趣,在这里和大家分享一下。
上手先来个demo
spring security 核心配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserdetailService userDetailsService;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置用户加载来源,同时设置密码不加密。Spring5以后security默认是需要加密的
auth.userDetailsService(userDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and()
.formLogin().loginProcessingUrl("/login").loginPage("/login.html").defaultSuccessUrl("/").permitAll().failureUrl("/errorPage").permitAll().and()
.logout().permitAll().and()
.rememberMe().tokenRepository(persistentTokenRepository).and()//开启remeberMe功能并且设置token持久化为数据库存储
.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**","/js/**");
}
//定义token存储持久化实现为Jdbc实现
@Bean
public PersistentTokenRepository createTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true); //默认是要在数据库建表的,如果你是第一次运行可以设置为true,后面就可以注释掉了
return jdbcTokenRepository;
}
}
MyUserdetailService
@Service
public class MyUserdetailService implements UserDetailsService {
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private UserInfoRoleMapper userInfoRoleMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfoDO userInfoDO = userInfoMapper.selectByName(username);
if(Objects.isNull(userInfoDO)){
throw new UsernameNotFoundException("用户名"+username+"不存在");
}
List<GrantedAuthority> list=new ArrayList<>();
List<UserInfoRoleDO> infoRoleDOS = userInfoRoleMapper.listByUserId(userInfoDO.getId().intValue());
infoRoleDOS.stream().forEach(o->{
String roleCode = roleMapper.selectById(o.getRoleId().intValue()).getRoleCode();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleCode);
list.add(simpleGrantedAuthority);
});
return new User(userInfoDO.getLoginName(),userInfoDO.getLoginName(),list);
}
}
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login">
<div>
用户名:<input type="text" name="username">
</div>
<div>
密码:<input type="password" name="password">
</div>
<div>
<label><input type="checkbox" name="remember-me"/>自动登录</label>
<button type="submit">立即登陆</button>
</div>
</form>
</body>
</html>
到这里简单remeberme功能就配置完成了。可以发现在勾选了自动登录以后数据库中会出现一条记录。

- 需要特别注意的是,一旦使用PersistentTokenRepository的实现完成remeberme的功能,在登陆的时候就必须要勾选自动登录,不然没有办法正常登录。RememberMeServices的默认实现是TokenBasedRememberMeServices这个方式的实现不能实现分布式的session,但是支持不勾选自动登录可以正常使用。
源码分析
remeberme的认证过程

- AbstractAuthenticationProcessingFilter 进入doFilter以后开始进行账号密码的验证。通过账号密码验证以后。调用successfulAuthentication方法,开始设置cookie。调用RemeberServices的loginSuccess设置自动登录cookie。
- 注入基于数据库的token管理的过程。我们可以发现TokenBasedRememberMeServices,作为默认的RemeberMeServices实现,并不支持存储在数据库中。需要手动指定一个Persistent的实现。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and()
.formLogin().loginProcessingUrl("/login").loginPage("/login.html").defaultSuccessUrl("/").permitAll().failureUrl("/errorPage").permitAll().and()
.logout().permitAll().and()
.rememberMe().tokenRepository(persistentTokenRepository).and()
.rememberMe().and()
.csrf().disable();
}
@Bean
public PersistentTokenRepository createTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
那么PersistentTokenRepository又是如何代替默认的实现呢?可以看到RememberMeConfigurer中init()中调用了getRememberMeServices()方面。getRememberMeServices()中有一段createRememberMeServices()就是用初始化选择加载哪一个RemebermeServices实现的。
private AbstractRememberMeServices createRememberMeServices(H http, String key)
throws Exception {
--这里是对remeberMeServices实现的一个选择
return this.tokenRepository == null
? createTokenBasedRememberMeServices(http, key)
: createPersistentRememberMeServices(http, key);
}
//这里就是WebSecurityConfig注入JdbcTokenRepositoryImpl的入口
public RememberMeConfigurer<H> tokenRepository(
PersistentTokenRepository tokenRepository) {
this.tokenRepository = tokenRepository;
return this;
}