赞
踩
上一篇文章我们了解到了,rememberMe 将令牌保存在cookie中,每次请求服务端都会去验证令牌的有效性以及合法性,但是,这样就会产生一个问题,如果浏览器中的令牌被人盗用了,那岂不是非常不安全。
关于这个问题,Security也给出了解决方案,那就是持久化令牌,一旦令牌被盗用,用户就可以及时感知,然后重新登陆,覆盖掉之前的令牌。
在Spring Security中提供了一个持久化token的仓库接口,就是PersistentTokenRepository这个,他有两个实现类。
第一个,就是基于内存来弄,但是我们一般不这么搞,来看看,基于jdbc存入数据库持久化怎么弄。
进入到JdbcTokenRepository这个实现类中看看,注释我直接写到源码里面把,大家注意看。
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository { //重点是这个,创建一个persistent_logins 表,用来记录token。里面有四个字段,分别解释一下 //username 用户名 //series 生成的字符串,用来找到token //token 具体的令牌 //last_used 最后一次使用的时间 public static final String CREATE_TABLE_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)"; public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?"; public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"; public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?"; public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?"; private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?"; private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"; private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?"; …… }
然后,我们在数据库创建上面那个persistent_logins 表,然后创建一个工程,进行测试。
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
然后简单配置一下,Security.config
@Configuration public class SecurityConfig { @Autowired DataSource dataSource; //注入数据源 @Bean JdbcTokenRepositoryImpl jdbcTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } /** * 这里用户简单点,我就写内存了 * @return */ @Bean UserDetailsService userDetailsService(){ InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").build()); return inMemoryUserDetailsManager; } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(a->a .requestMatchers("/auth/*").fullyAuthenticated() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .rememberMe(r -> r. key("tongzhou") //一直开启rememberMe记录,这样前端选不选都会记录 .alwaysRemember(true) //设置token仓库,我们这里就是通过jdbc存入数据库中 .tokenRepository(jdbcTokenRepository()) ) .csrf(c->c.disable()); return http.build(); } }
上面也写的比较清楚了,就不多说了,重点是看数据库,存在了一条记录。
然后关闭浏览器,去postman测一下,把remember-me复制过去
这样,也是可以的,但是我们关掉浏览器,然后重新打开,再去访问/hello这个接口,就会让你重新登陆了。
可以看到,series值变了,token值也变了,这是因为我刚刚用浏览器过期的访问去请求他的地址,数据库直接把那条数据删掉了,后面分析源码,可以看一下。
ok了,这就没问题了,后台爆出被攻击信息。
Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
postman测的时候,要清掉JESSIONID,要清掉,采用rememberMe的认证信息后,他会把JSONID也认证了,所以有时候,我们重新登陆后,工具还能访问。清掉JSONID后,只用原本的RememberMe就登不上了。
我们看一下,RememberMeServices ,这个服务接口提供了下面三个方法,
autoLogin 从请求中提取需要的参数
loginFail 自动登录失败后的回调
loginSuccess 自动登录成功后的回调。
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);
}
我们来下下它的实现类,主要看AbstractRememberMeServices,extractRememberMeCookie这个方法,获取到请求中的remember-me这个值,如果为null,直接返回。接着,看一下它的长度是不是0,如果是,则将前台的remember-me设置为null。
接下来,就是decodeCookie,对进来的rememberMeCookie进行解析,
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = this.extractRememberMeCookie(request); if (rememberMeCookie == null) { return null; } else { this.logger.debug("Remember-me cookie detected"); if (rememberMeCookie.length() == 0) { this.logger.debug("Cookie was empty"); this.cancelCookie(request, response); return null; } else { try { String[] cookieTokens = this.decodeCookie(rememberMeCookie); UserDetails user = this.processAutoLoginCookie(cookieTokens, request, response); this.userDetailsChecker.check(user); this.logger.debug("Remember-me cookie accepted"); return this.createSuccessfulAuthentication(request, user); } catch (CookieTheftException var6) { this.cancelCookie(request, response); throw var6; } catch (UsernameNotFoundException var7) { this.logger.debug("Remember-me login was valid but corresponding user not found.", var7); } catch (InvalidCookieException var8) { this.logger.debug("Invalid remember-me cookie: " + var8.getMessage()); } catch (AccountStatusException var9) { this.logger.debug("Invalid UserDetails: " + var9.getMessage()); } catch (RememberMeAuthenticationException var10) { this.logger.debug(var10.getMessage()); } this.cancelCookie(request, response); return null; } } }
附上解析代码,其实就是三部分,第一部分用户名,第二部分,时间戳,第三部分就是加密摘要。
protected String[] decodeCookie(String cookieValue) throws InvalidCookieException { for(int j = 0; j < cookieValue.length() % 4; ++j) { cookieValue = cookieValue + "="; } String cookieAsPlainText; try { cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes())); } catch (IllegalArgumentException var7) { throw new InvalidCookieException("Cookie token was not Base64 encoded; value was '" + cookieValue + "'"); } String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, ":"); for(int i = 0; i < tokens.length; ++i) { try { tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8.toString()); } catch (UnsupportedEncodingException var6) { this.logger.error(var6.getMessage(), var6); } } return tokens; }
这部分都拿到后,接下来就是对比了processAutoLoginCookie,这是一个抽象类,具体的实现,去它的子类看看。这里我们使用的数据库,所以看PersistentTokenBasedRememberMeServices
cookieTokens 参数不等于2就报错,这是因为里面有两个参数,series和token,要根据series去数据库里面查看token的值。如果查出来为null ,那就是没有,自动登陆失败。
如果查出来的值和现有的不一样,那就说明token已经泄露了,抛出一个错误,会在控制台打出来,可以去上面看看,是不是一样。
接着,再去看token的时间有没有过期,也没有过期的话,就会
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
用户名,series,不变,使用现在的时间,new Date,重新生成token。
this.tokenRepository.updateToken() 更新token,然后将新的token返回。
if (token == null) { throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries); } else if (!presentedToken.equals(token.getTokenValue())) { 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 { this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries())); PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date()); try { this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); 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"); } return this.getUserDetailsService().loadUserByUsername(token.getUsername()); }
这就是他的认证了,接下来,我们看看它的成功后回调:
onLoginSuccess
登陆成功后创建一个PersistentRememberMeToken对象,调用addCookie将它存入数据库中。
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug(LogMessage.format("Creating new persistent login for user %s", username));
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。