自动登录时将用户的登录信息保存在用户浏览器的cookie中,当用户下次访问时,自动实现校验并建立登录态的一种机制。
Spring Security提供了两种非常好的令牌:
- 用散列算法加密用户必要的登录信息并生成令牌。
- 数据库等持久性数据存储机制用的持久化令牌。
一:散列加密方案
首先在静态页面加上
<div>
<p><input name="remember-me" type="checkbox" value="true">记住我</p>
</div>
接着在Spring Security的配置类配置.
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsServiceImpl myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasAuthority("admin")
//开放/captcha.jpg的访问权限
.antMatchers("/app/api/**","/captcha.jpg").permitAll()
.antMatchers("/user/api/**").hasAuthority("user")
.anyRequest().authenticated()
.and().formLogin()
.failureHandler(new SecurityAuthenticationFailureHandler())
.successHandler(new SecurityAuthenticationSuccessHandler())
.loginPage("/myLogin.html")
.loginProcessingUrl("/login")
.permitAll()
.and().rememberMe()
.userDetailsService(myUserDetailsService)
// 1. 散列加密方案
.key("blurool")
.and().csrf().disable();
// 将过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
如果没有指定时,key是一个UUID字符串,Spring Security会在每次表单登录成功后更新此令牌。这将导致每次重启服务后,key都会重新生成,使得重启之前的所有自动登录cookie失效。除此之外,在多实例部署的情况下,由于实例间的key并不相同,所以当用户访问系统的另一个实例时,自动登录策略就会失效。解决办法是指定一个唯一的key。
二:持久化令牌方案
在持久化令牌方案中,最核心的是series和token两个值,它们都是用MD5散列过的随机字符串。不同的是,series仅在用户使用密码重新登录时更新,而token会在每个新的session中都重新生成。这样设计,可以解决散列加密方案中一个令牌可以同时在多端登录的问题。每个会话都会引发token的更新,即每个token仅支持单实例登录。
其次,自动登录不会导致series变更,而每次自动登录都需要同时验证series和token两个值,当该令牌还未使用过自动登录就被盗取时,系统会在非法用户验证通过后刷新token值,此时在合法用户的浏览器中,该token值已经失效。当合法用户使用自动登录时,由于该series对应的token不同,系统可以推断该令牌可能已被盗用,从而做一些处理。例如,清理该用户的所有自动登录令牌,并通知该用户可能已被盗号等。
持久化令牌配置如下
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
MyUserDetailsServiceImpl myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/api/**").hasAuthority("admin")
//开放/captcha.jpg的访问权限
.antMatchers("/app/api/**","/captcha.jpg").permitAll()
.antMatchers("/user/api/**").hasAuthority("user")
.anyRequest().authenticated()
.and().formLogin()
.failureHandler(new SecurityAuthenticationFailureHandler())
.successHandler(new SecurityAuthenticationSuccessHandler())
.loginPage("/myLogin.html")
.loginProcessingUrl("/login")
.permitAll()
.and().rememberMe()
.userDetailsService(myUserDetailsService)
//cookie通过Base64解码:6KqcpfMU+ykV5T8o+lTWXg==:UhOurgglqFMoGYpyMutUpg==
//冒号前是series,冒号后是token
.tokenRepository(jdbcTokenRepository) // 持久化令牌方案
// 7天有效期
.tokenValiditySeconds(60 * 60 * 24 * 7)
.and().csrf().disable();
// 将过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
对于上面配置的tokenRepository持久化令牌,我们可以按照自己的方式实现PersistentTokenRepository 接口,也可以使用Spring Security提供的JDBC方案实现。上面使用JDBC方案。
public interface PersistentTokenRepository {
void createNewToken(PersistentRememberMeToken token);
void updateToken(String series, String tokenValue, Date lastUsed);
PersistentRememberMeToken getTokenForSeries(String seriesId);
void removeUserTokens(String username);
}
显然,两种方案都存在被盗取导致身份被暂时利用的可能,如果有更高的安全性需求,建议使用Spring Security提供的令牌持久化方案。当然,最安全的方式还是尽量不使用自动登录,但很多时候,在实际开发中,优质体验比不可预期的安全风险要更为优先。
如果决定提供自动登录功能,就应当限制cookie登录时的部分执行权限。例如,修改密码,修改邮箱(防止找回密码),查看隐私信息(如完整的手机号码,银行卡号等)等,校验登录密码或设置独立密码来做二次校验也是不错的方案。
三:注销登录
配置如下:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
MyUserDetailsServiceImpl myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/api/**").hasAuthority("admin")
//开放/captcha.jpg的访问权限
.antMatchers("/app/api/**","/captcha.jpg").permitAll()
.antMatchers("/user/api/**").hasAuthority("user")
.anyRequest().authenticated()
.and().formLogin()
.failureHandler(new SecurityAuthenticationFailureHandler())
.successHandler(new SecurityAuthenticationSuccessHandler())
.loginPage("/myLogin.html")
.loginProcessingUrl("/login")
.permitAll()
.and().rememberMe()
.userDetailsService(myUserDetailsService)
//6KqcpfMU+ykV5T8o+lTWXg==:UhOurgglqFMoGYpyMutUpg==
//冒号前是series,冒号后是token
.tokenRepository(jdbcTokenRepository) // 持久化令牌方案
// 7天有效期
.tokenValiditySeconds(60 * 60 * 24 * 7)
.and()
.logout()
//指定接收注销请求的路由
.logoutUrl("/myLogout")
// 注销成功,重定向到该路径下
.logoutSuccessUrl("/")
//使该用户的HttpSession失效
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID","remember-me")
.and().csrf().disable();
// 将过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}