SpringSecurity源码导读(一)-Remeberme

225 阅读3分钟

在使用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功能就配置完成了。可以发现在勾选了自动登录以后数据库中会出现一条记录。

说明我们的session其实已经存储下来了。

  • 需要特别注意的是,一旦使用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;
	}