前面我们介绍了rememberMe的实现原理,从中我们可以思考这样一个问题,如果我们的cookie被非法用户获取,然后携带这个cookie进行访问我们的项目中的内容,就会导致非法用户登录。这个问题怎么解决呢?
RememberMe进阶
在之前我们提到了cookie的生成以及验证都是在TokenBasedRememberMeServices
这个类里面完成的,RememberMeServices
这个接口还有一个实现类PersistentTokenBasedRememberMeServices
PersistentTokenBasedRememberMeServices
就是一个进阶版本的rememberMe的实现,这个生成的cookie是Series
和TokenValue
组成的
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
//这个生成的cookie
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,
HttpServletResponse response) {
setCookie(new String[] {
token.getSeries(), token.getTokenValue() },
getTokenValiditySeconds(), request, response);
}
如果下次会话过期了之后就会走下面这块的认证逻辑,这里面的PersistentTokenRepository
的实现默认是基于内存的实现
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
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) + "'");
}
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException(
"No persistent token found for series id: " + presentedSeries);
}
// We have a match for this user/series combination
//这里对比内存中token对应的value和cookie里面的进行对比,如果相同则认证通过
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(
messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
// Token also matches, so login is valid. Update the token value, keeping the
// *same* series number.
if (logger.isDebugEnabled()) {
logger.debug("Refreshing persistent login token for user '"
+ token.getUsername() + "', series '" + token.getSeries() + "'");
}
//下面这一块的逻辑就是把cookie进行一个更新,也就是说一旦会话失效,如果使用了之前的cookie就会生成新
//的cookie,原阿里的cookie九无法使用了。这在一定程度上增加了安全性
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
//这里更新的时候series是不变的,变的是series对应的value值
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception e) {
logger.error("Failed to update token: ", e);
throw new RememberMeAuthenticationException(
"Autologin failed due to data access problem");
}
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
RememberMe的持久化令牌
对于前面的基于内存的记住我的功能,一旦项目重启了之后就需要重新登录,这有的时候是不符合要求的,我们需要把这种基于内存实现的方式,放到数据库里面进行实现。
默认的是基于内存的,我们可以自定义基于数据库的实现。
在配置类中添加
@Bean
protected PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices(){
PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices =
// new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), myUserDetailService, new InMemoryTokenRepositoryImpl());
new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), myUserDetailService, jdbcTokenRepository());
return persistentTokenBasedRememberMeServices;
}
@Bean
protected JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
这个时候启动项目就会生成表结构
此时我登录用户就会生成对应的cookie持久化到数据库
这个时候我重启项目然后刷新页面一样是不需要重新登录的。重启的时候要把这句话注释掉
// tokenRepository.setCreateTableOnStartup(true);
刷新之后
可以发现token的值变了,series值没变,这和我们上面的分析是一致的。
对于前后端分离项目,如果要设置rememberMe,就需要修改下面的部分
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (alwaysRemember) {
return true;
}
String paramValue = request.getParameter(parameter);
if (paramValue != null) {
if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
return true;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Did not send remember-me cookie (principal did not set parameter '"
+ parameter + "')");
}
return false;
}
我们需要写个子类覆盖父类的rememberMeRequested方法。具体的做法可以参考不良人的视频