这里再多说下,对于记住我功能,同样可以查看RememberMeConfigurer,看看是怎么实现的:
public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {
private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";
private AuthenticationSuccessHandler authenticationSuccessHandler;
private String key;
private RememberMeServices rememberMeServices;
private LogoutHandler logoutHandler;
private String rememberMeParameter = "remember-me";
private String rememberMeCookieName = "remember-me";
private String rememberMeCookieDomain;
private PersistentTokenRepository tokenRepository;
private UserDetailsService userDetailsService;
private Integer tokenValiditySeconds;
private Boolean useSecureCookie;
private Boolean alwaysRemember;
且不说别的,就这些类字段就能看到很多信息,比如对应参数名称默认是“remember-me”;类似的也有个RememberMeService,是否总是记住我(alwaysRemember
)。。等等。可以按照自己的需求做自定义的配置。该类有个init
方法,顾名思义,会做一些初始化操作,我们可以看看记住我的service是怎么操作的。
public void init(H http) throws Exception {
this.validateInput();
String key = this.getKey();
// 获取service
RememberMeServices rememberMeServices = this.getRememberMeServices(http, key);
http.setSharedObject(RememberMeServices.class, rememberMeServices);
LogoutConfigurer<H> logoutConfigurer = (LogoutConfigurer)http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && this.logoutHandler != null) {
logoutConfigurer.addLogoutHandler(this.logoutHandler);
}
RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(key);
authenticationProvider = (RememberMeAuthenticationProvider)this.postProcess(authenticationProvider);
http.authenticationProvider(authenticationProvider);
this.initDefaultLoginFilter(http);
}
同样地,有个get方法,进一步查看this.getRememberMeServices
:
private RememberMeServices getRememberMeServices(H http, String key) throws Exception {
// 如果自定义了service
if (this.rememberMeServices != null) {
if (this.rememberMeServices instanceof LogoutHandler && this.logoutHandler == null) {
this.logoutHandler = (LogoutHandler)this.rememberMeServices;
}
return this.rememberMeServices;
} else { // 否则创建service
AbstractRememberMeServices tokenRememberMeServices = this.createRememberMeServices(http, key);
tokenRememberMeServices.setParameter(this.rememberMeParameter);
tokenRememberMeServices.setCookieName(this.rememberMeCookieName);
if (this.rememberMeCookieDomain != null) {
tokenRememberMeServices.setCookieDomain(this.rememberMeCookieDomain);
}
if (this.tokenValiditySeconds != null) {
tokenRememberMeServices.setTokenValiditySeconds(this.tokenValiditySeconds);
}
if (this.useSecureCookie != null) {
tokenRememberMeServices.setUseSecureCookie(this.useSecureCookie);
}
if (this.alwaysRemember != null) {
tokenRememberMeServices.setAlwaysRemember(this.alwaysRemember);
}
tokenRememberMeServices.afterPropertiesSet();
this.logoutHandler = tokenRememberMeServices;
this.rememberMeServices = tokenRememberMeServices;
return tokenRememberMeServices;
}
}
虽然方法、变量都比较长,但是其实是很好懂的,对于没有对应配置的情况下,调用this.createRememberMeServices
来创建。再进一步去看:
private AbstractRememberMeServices createRememberMeServices(H http, String key) {
return this.tokenRepository == null ? this.createTokenBasedRememberMeServices(http, key) : this.createPersistentRememberMeServices(http, key);
}
private AbstractRememberMeServices createTokenBasedRememberMeServices(H http, String key) {
UserDetailsService userDetailsService = this.getUserDetailsService(http);
return new TokenBasedRememberMeServices(key, userDetailsService);
}
private AbstractRememberMeServices createPersistentRememberMeServices(H http, String key) {
UserDetailsService userDetailsService = this.getUserDetailsService(http);
return new PersistentTokenBasedRememberMeServices(key, userDetailsService, this.tokenRepository);
}
可以看到,这里是根据this.tokenRepository
是否为null
来选择调用下边两个方法,如果没有设置 tokenRepository
,那么就新建一个 TokenBasedRememberMeServices,否则就新建一个PersistentTokenBasedRememberMeServices,从名字就可以看出来,前者是非持久化的,后者是持久化的。这两者都间接实现了RememberMeServices接口。
从源码也能看出来,对于前者,查看它的onLoginSuccess
方法(这是RememberMeServices接口中的方法,用于登录成功时执行记住我的操作):
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = this.retrieveUserName(successfulAuthentication);
String password = this.retrievePassword(successfulAuthentication);
// ...省略无关代码
int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
String signatureValue = this.makeTokenSignature(expiryTime, username, password);
this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
}
对于这种非持久化的方案,它是把用户名、失效时间、签名信息等编码到Cookie中,那么以后通过这个Cookie就可以登录了,相对来说是比较简单易用的。不过这里是把用户名直接编码到Cookie中的,对于不想泄露用户名的应用来说可能安全性不够,(当然通过Cookie/Session这种方式也不是绝对安全的,可以自行了解更多更安全的认证方式)。
查看PersistentTokenBasedRememberMeServices的持久化实现方法:
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
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);
}
}
可以看到这里也使用了用户名username
,但是这里是先执行了this.tokenRepository.createNewToken(persistentToken);
,再把persistentToken
添加到Cookie中。如果使用过JPA的同学应该知道这个xxxRepository就是做持久化工作的,比如访问数据库啥的。查看这个createNewToken
方法,发现其是PersistentTokenRepository接口的一个方法,该方法有两个默认实现:
顾名思义,一个是持久化到内存,一个是持久化到数据库。这取决于我们的配置,因为如果我们选择持久化方案,就得配置一个tokenRepository
,因为前边看过了,只有在配置中这个参数不为空才会选择持久化方案,而为空就会采用非持久化方案。
其实这里可以自己看下基于内存的实现,其实就是建立一个HashMap保存而已,没什么神奇的。
private final Map<String, PersistentRememberMeToken> seriesTokens = new HashMap();
public synchronized void createNewToken(PersistentRememberMeToken token) {
PersistentRememberMeToken current = (PersistentRememberMeToken)this.seriesTokens.get(token.getSeries());
if (current != null) {
throw new DataIntegrityViolationException("Series Id '" + token.getSeries() + "' already exists!");
} else {
this.seriesTokens.put(token.getSeries(), token);
}
}
对于持久化到数据库的实现(JdbcTokenRepositoryImpl)其实更简单,就是执行一条SQL:
public void createNewToken(PersistentRememberMeToken token) {
this.getJdbcTemplate().update(this.insertTokenSql, new Object[]{token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate()});
}
查看这个类的类字段部分:
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
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 = ?";
private String removeUserTokensSql = "delete from persistent_logins where username = ?";
private boolean createTableOnStartup;
可以看到它内置了一些SQL语句,提供对数据库表的增删改查操作,并且提供了一个boolean createTableOnStartup
,表示是否在启动时新建对应数据库表。
其实这里还有个疑问,就是前边说的,持久化方案也会保存用户名,并且也是写入Cookie,看起来它相比非持久化方式,非但没有提供更多的安全性保证,反而多了一些数据库操作。来看来它具体是怎么做的,回到PersistentTokenBasedRememberMeServices的onLoginSuccess
方法:
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
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);
}
}
这里它把用户名(username
)、当前时间(new Date()
)还有两个序列数(this.generateSeriesData(), this.generateTokenData()
)封装为PersistentRememberMeToken对象,随后调用this.addCookie
将其放入Cookie中,查看这个addCookie
方法:
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
this.setCookie(new String[]{token.getSeries(), token.getTokenValue()}, this.getTokenValiditySeconds(), request, response);
}
可以看到,这里并没有放对象中的用户名,只放了对象中的两个序列数,这也就防止了用户名在Cookie中来回传递。认证的时候,只需要借助这两个序列数查对应的数据库表,就能得到用户名和过期时间,进而达到记住我的功能。
总结
记住我功能使用Cookie来实现,默认使用的是非持久化方案,它会把用户名写在Cookie里。如果需要持久化方案,就需要配置一个PersistentTokenRepository,这里对于持久化方案有两种选择:基于内存和基于数据库。基于内存的方案是建立一个HashMap来保存,基于数据库的是新建一个专用表。持久化方案会生成序列数,把序列数保存在Cookie中,验证时通过序列数查数据库表 来得到用户名。
此外,官方文档也对记住我功能有很详细的描述
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-rememberme