一、前言
上一篇介绍了关于自动登录无持久化方式的内容,通过源码我们知道这种方式的cookie中包含了用户名、密码,这个从安全性上讲是存在风险的,那还有另一种方式就是通过数据库持久化cookie所包含的信息,并且是跟用户名、密码不相关的内容。
二、实现
Spring Security中已经定义好了对于数据库操作的类JdbcTokenRepositoryImpl,所以不需要我们自己定义了,但是存储数据的表默认是没有的需要创建。那在原有的基础上在SecurityConfig类中要增加一些内容:
// 导入数据源
@Autowired
DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF(跨站请求伪造)禁用,默认开启,会检测请求中是否包含令牌,没有则拒绝并返回403
.authorizeRequests()
// 对静态资源放行
.mvcMatchers(HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.anyRequest().authenticated()// 除了上面其他都必须鉴权
.and()
.formLogin().loginPage("/loginPage")// 未认证时访问跳转登录页面
.loginProcessingUrl("/doLogin")// 表单登录url设置,默认/login
// 登录成功跳转
.defaultSuccessUrl("/main",true)
.permitAll()
.and()
.logout().permitAll()
.and()
.rememberMe() // 自动登录
.tokenRepository(jdbcTokenRepository()) // 添加;
}
// 注入JdbcTokenRepositoryImpl
@Bean
public JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 表示自动创建表,但是如果重复启动则会报异常,提示表存在了
// jdbcTokenRepository.setCreateTableOnStartup(true);
jdbcTokenRepository.setDataSource(dataSource); // 设置数据源
return jdbcTokenRepository;
}
要注入数据源,还有操作数据库的bean,上面有个注释提到的在jdbcTokenRepository中是可以设置自动创建数据表,但是问题也在注释说明了,那我们其实可以把JdbcTokenRepository中创建表的语句拷贝出来自己手动建表就行了。
// 建表的方法
protected void initDao() {
if (this.createTableOnStartup) {
this.getJdbcTemplate().execute("create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)");
}
}
建表sql:
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
建完表然后运行,登录成功后查看数据库:
三、细节
下面来看下这种方式的cookie是一些什么内容,上面提到过这种方式的cookie内容与用户名、密码无关,具体源码位于PersistentTokenBasedRememberMeServices:
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
// 创建token
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
// 添加到数据库
this.tokenRepository.createNewToken(persistentToken);
// 创建cookie并添加到response
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
protected String generateSeriesData() {
byte[] newSeries = new byte[this.seriesLength];
this.random.nextBytes(newSeries);
return new String(Base64.getEncoder().encode(newSeries));
}
protected String generateTokenData() {
byte[] newToken = new byte[this.tokenLength];
this.random.nextBytes(newToken);
return new String(Base64.getEncoder().encode(newToken));
}
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
this.setCookie(new String[]{token.getSeries(), token.getTokenValue()}, this.getTokenValiditySeconds(), request, response);
}
其中PersistentRememberMeToken数据表persistent_logins对应表示持久化的token的实体类,包含了用户名,序列号、token、创建token的时间,其中序列号和token都是随机生成的,并且在addCookie中创建cookie用到的就只是序列号和token,就没有用户名、密码。
那这种方式自动登录时又是怎么校验的,还是在这个类中的processAutoLoginCookie方法:
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
// 获取传过来cookie中的序列号
String presentedSeries = cookieTokens[0];
// 获取传过来cookie中的token
String presentedToken = cookieTokens[1];
// 通过序列号查询数据表,得到PersistentRememberMeToken对象
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {// token值判断
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {// 有效时间判断
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
}
// 满足上面判断的话,就说明满足自动登录了
// 创建新的PersistentRememberMeToken,主要是更新时间
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
try {
// 更新数据表
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
// 创建cookie并添加到response
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
// 最后通过persistentRememberMeToken中的用户名获取用户数据
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}
上面的注释已经把过程写的很清楚了,主要就是通过cookie中的序列号查询数据表中已有的PersistentRememberMeToken数据进行比对,满足条件即可自动登录。
这种方式重启服务器也不会影响到此前用户的cookie的有效性了,只要是在期限内重启服务器也能正常自动登录。