原理分析
从 RememberMeServices 接口开始介绍。
RememberMeServices 接定义如下:
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}
这里一共定义了三个方法:
-
autoLogin 方法可以从请求中提取出需要的参数,完成自动登录功能。
-
loginFail 方法是自动登录失败的回调。
-
loginSuccess方法是自动登录成功的回调。
RememberMeServices 接口的继承关系如图 6-7 所示。

NullRememberMeServices 是一个空的实现,这里不做讨论,我们来重点分析另外三个实现类。
AbstractRememberMeServices
AbstractRememberMeServices 对于 RememberMeServices 接口中定义的方法提供了基本的实现,这里就以接口中定义的方法为思路,分析 AbstractRememberMeServices 中的具体实现。
首先我们来看 autoLogin 及其相关方法:
@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
cancelCookie(request, response);
throw cte;
}
catch (UsernameNotFoundException noUser) {
logger.debug("Remember-me login was valid but corresponding user not found.",
noUser);
}
catch (InvalidCookieException invalidCookie) {
logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
}
catch (AccountStatusException statusInvalid) {
logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
}
catch (RememberMeAuthenticationException e) {
logger.debug(e.getMessage());
}
cancelCookie(request, response);
return null;
}
protected String extractRememberMeCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if ((cookies == null) || (cookies.length == 0)) {
return null;
}
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
return cookie.getValue();
}
}
return null;
}
protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
for (int j = 0; j < cookieValue.length() % 4; j++) {
cookieValue = cookieValue + "=";
}
try {
Base64.getDecoder().decode(cookieValue.getBytes());
}
catch (IllegalArgumentException e) {
throw new InvalidCookieException(
"Cookie token was not Base64 encoded; value was '" + cookieValue
+ "'");
}
String cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes()));
String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText,
DELIMITER);
for (int i = 0; i < tokens.length; i++)
{
try
{
tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8.toString());
}
catch (UnsupportedEncodingException e)
{
logger.error(e.getMessage(), e);
}
}
return tokens;
}
protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) {
logger.debug("Cancelling cookie");
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
cookie.setPath(getCookiePath(request));
if (cookieDomain != null) {
cookie.setDomain(cookieDomain);
}
if (useSecureCookie == null) {
cookie.setSecure(request.isSecure());
}
else {
cookie.setSecure(useSecureCookie);
}
response.addCookie(cookie);
}
protected Authentication createSuccessfulAuthentication(HttpServletRequest request,
UserDetails user) {
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, user,
authoritiesMapper.mapAuthorities(user.getAuthorities()));
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
autoLogin 方法主要功能就是从当前请求中提取出令牌信息,根据令牌信息完成自动登录功能,登录成功之后会返回一个认证后的 Authentication 对象,我们来看一下该方法的具体实现:
-
首先调用 extractRememberMeCookie 方法从当前请求中提取出需要的 Cookie 信息,即 remember-me 对应的值。如果这个值为 null,表示本次请求携带的 Cookie 中没有 remember-me,这次不需要自动登录,直接返回 null 即可。如果 remember-me 对应的值长度为 0,则在返回 null 之前,执行一下 cancelCookie 函数,将 Cookie 中 remember-me 的值置为 null。
-
接下来调用 decodeCookie 方法对获取到的令牌进行解析。具体方式是,先用 Base64 对令牌进行还原(如果令牌字符串长度不是 4 的倍数,则在令牌末尾补上一个或者多个 “=”,以使其长度变为 4 的倍数,之所以要是 4 的倍数,这和 Base64 编解码的原理有关,感兴趣的读者可以自行学习 Base64 编解码的原理,并不难),还原之后的字符串分为三部分,三部分之间用 “:” 隔开,第一部分是当前登录用户名,第二部分是时间戳,第三部分是一个签名。也就是说,我们一开始在浏览器中看到的 remember-me令牌,其实是一个 Base64 编码后的字符串,解码后的信息包含三部分,读者可以根据 decodeCookie 中的方法自行尝试对令牌进行解码。最后将这三部分分别提取出来组成一个数组返回。
-
调用 processAutoLoginCookie 方法对 Cookie 进行验证,如果验证通过,则返回登录用户对象,然后对用户状态进行检验(账户是否可用、账户是否锁定等)。processAutoLoginCookie 方法是一个抽象方法,具体实现在 AbstractRememberMeServices 的子类中。
-
最后调用 createSuccessfulAuthentication 方法创建登录成功的用户对象,不同于使用用户名/密码登录,本次登录成功后创建的用户对象类型是 RememberMeAuthenticationToken。
接下来我们再来看一下自动登录成功和自动登录失败的回调:
@Override
public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
logger.debug("Interactive login attempt was unsuccessful.");
cancelCookie(request, response);
onLoginFail(request, response);
}
protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {
}
@Override
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
protected abstract void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication);
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;
}
-
登录失败时,首先取消 Cookie 的设置,然后调用 onLoginFail 方法完成失败处理,onLoginFail 方法是一个空方法,如果有需要,开发者可以自行重写该方法,一般来说不需要重写。
-
登录成功时,会首先调用 rememberMeRequested 方法,判断当前请求是否开启了自动登录。开发者可以在服务端配置 alwaysRemember,这样无论前端参数是什么,都会开启自动登录,如果开发者没有配置 alwaysRemember,则根据前端传来的 remember-me 参数进行判断,remember-me 参数的值如果是 true、on(默认)、yes 或者 1,表示开启自动登录。如果开启了自动登录,则调用 onLoginSuccess 方法进行登录成功的处理。onLoginSuccess 是一个抽象方法,具体实现在 AbstractRememberMeServices 的子类中。
最后再来看 AbstractRememberMeServices 中一个比较重要的方法 setCookie,在自动登录成功后,将调用该方法把令牌信息放入响应头中并最终返回到前端:
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request,
HttpServletResponse response) {
String cookieValue = encodeCookie(tokens);
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setMaxAge(maxAge);
cookie.setPath(getCookiePath(request));
if (cookieDomain != null) {
cookie.setDomain(cookieDomain);
}
if (maxAge < 1) {
cookie.setVersion(1);
}
if (useSecureCookie == null) {
cookie.setSecure(request.isSecure());
}
else {
cookie.setSecure(useSecureCookie);
}
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
protected String encodeCookie(String[] cookieTokens) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cookieTokens.length; i++) {
try
{
sb.append(URLEncoder.encode(cookieTokens[i], StandardCharsets.UTF_8.toString()));
}
catch (UnsupportedEncodingException e)
{
logger.error(e.getMessage(), e);
}
if (i < cookieTokens.length - 1) {
sb.append(DELIMITER);
}
}
String value = sb.toString();
sb = new StringBuilder(new String(Base64.getEncoder().encode(value.getBytes())));
while (sb.charAt(sb.length() - 1) == '=') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
-
首先调用 encodeCookie 方法对要返回到前端的数据进行 Base64 编码,具体方式是将数组中的数据拼接成一个字符串并用 “:” 隔开,然后对其进行 Base64 编码。
-
将编码后的字符串放入 Cookie 中,并配置 Cookie 的过期时间、path、domain、secure、httpOnly 等属性,最终将配置好的 Cookie 对象放入响应头中。
这便是 AbstractRememberMeServices 中的几个主要方法,还有其他一些辅助的方法都比较简单,读者可以自行研究。
TokenBasedRememberMeServices
TokenBasedRememberMeServices 是 AbstractRememberMeServices 的实现类之一,在 6.2 节中,我们讲解 RememberMe 的基本用法时,最终起作用的就是 TokenBasedRememberMeServices。 作为 AbstractRememberMeServices 的子类,TokenBasedRememberMeServices 最重要的方法就是对 AbstractRememberMeServices 中所定义的两个抽象方法 processAutoLoginCookie 和 onLoginSuccess 的实现。
我们先来看 processAutoLoginCookie 方法:
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 3) {
throw new InvalidCookieException("Cookie token did not contain 3"
+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
long tokenExpiryTime;
try {
tokenExpiryTime = new Long(cookieTokens[1]);
}
catch (NumberFormatException nfe) {
throw new InvalidCookieException(
"Cookie token[1] did not contain a valid number (contained '"
+ cookieTokens[1] + "')");
}
if (isTokenExpired(tokenExpiryTime)) {
throw new InvalidCookieException("Cookie token[1] has expired (expired on '"
+ new Date(tokenExpiryTime) + "'; current time is '" + new Date()
+ "')");
}
// Check the user exists.
// Defer lookup until after expiry time checked, to possibly avoid expensive
// database call.
UserDetails userDetails = getUserDetailsService().loadUserByUsername(
cookieTokens[0]);
Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
+ " returned null for username " + cookieTokens[0] + ". "
+ "This is an interface contract violation");
// Check signature of token matches remaining details.
// Must do this after user lookup, as we need the DAO-derived password.
// If efficiency was a major issue, just add in a UserCache implementation,
// but recall that this method is usually only called once per HttpSession - if
// the token is valid,
// it will cause SecurityContextHolder population, whilst if invalid, will cause
// the cookie to be cancelled.
String expectedTokenSignature = makeTokenSignature(tokenExpiryTime,
userDetails.getUsername(), userDetails.getPassword());
if (!equals(expectedTokenSignature, cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '"
+ cookieTokens[2] + "' but expected '" + expectedTokenSignature + "'");
}
return userDetails;
}
protected String makeTokenSignature(long tokenExpiryTime, String username,
String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No MD5 algorithm available!");
}
return new String(Hex.encode(digest.digest(data.getBytes())));
}
processAutoLoginCookie 方法主要用来验证 Cookie 中的令牌信息是否合法:
-
首先判断 cookieTokens 长度是否为 3,不为 3 说明格式不对,则直接抛出异常。
-
从 cookieTokens 数组中提取出第 1 项,也就是过期时间,判断令牌是否过期,如果已经过期,则抛出异常。
-
根据用户名(cookieTokens 数组的第 0 项)查询出当前用户对象。
-
调用 makeTokenSignature 方法生成一个签名,签名的生成过程如下:首先将用户名、令牌过期时间、用户密码以及 key 组成一个字符串,中间用 “:” 隔开,然后通过 MD5 消息摘要算法对该符串进行加密,并将加密结果转为一个字符串返回。
-
判断第 4 步生成的签名和通过 Cookie 传来的签名是否相等(即 cookieTokens 数组的第 2 项),如果相等,表示令牌合法,则直接返回用户对象,否则抛出异常。
再来看登录成功的回调函数 onLoginSuccess:
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// If unable to find a username and password, just abort as
// TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username)) {
logger.debug("Unable to retrieve username");
return;
}
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
logger.debug("Unable to obtain password for user: " + username);
return;
}
}
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
String signatureValue = makeTokenSignature(expiryTime, username, password);
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
tokenLifetime, request, response);
if (logger.isDebugEnabled()) {
logger.debug("Added remember-me cookie for user '" + username
+ "', expiry: '" + new Date(expiryTime) + "'");
}
}
-
在这个回调中,首先获取用户名和密码信息,如果用户密码在用户登录成功后已经从 successfulAuthentication 对象中擦除了,则从数据库中重新加载出用户密码。
-
计算出令牌的过期时间,令牌默认有效期是两周。
-
根据令牌的过期时间、用户名以及用户密码,计算出一个签名。
-
调用 setCookie 方法设置 Cookie,第一个参数是一个数组,数组中一共包含三项:用户名、过期时间以及签名,在 setCookie 方法中会将数组转为字符串,并进行 Base64 编码后响应给前端。
看完 processAutoLoginCookie 和 onLoginSuccess 两个方法的实现,相信读者对于令牌的生成和校验己经非常清楚了,这里再总结一下:
当用户通过用户名/密码的形式登录成功后,系统会根据用户的用户名、密码以及令牌的,过期时间计算出一个签名,这个签名使用 MD5 消息摘要算法生成,是不可逆的。然后再将用户名、令牌过期时间以及签名拼接成一个字符串,中间用 “:” 隔开,对拼接好的字符串进行 Base64 编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当用户关闭浏览器再次打开,访问系统资源时会自动携带上 Cookie 中的令牌,服务端拿到 Cookie 中的令牌后,先进行 Base64 解码,解码后分别提取出令牌中的三项数据:接着根据令牌中的数据判断令牌是否己经过期,如果没有过期,则根据令牌中的用户名查询出用户信息:接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示令牌是合法令牌,自动登录成功,否则自动登录失败。
PersistentTokenBasedRememberMeServices
PersistentTokenBasedRememberMeServices 类作为 AbstractRememberMeServices 的另一个实现类,在 6.3 节的案例中,使用的就是 PersistentTokenBasedRememberMeServices。
在持久化令牌中,存储在数据库中的数据被封装成了一个对象 PersistentRememberMeToken,其定义如下:
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;
//省略 getter/setter
}
username 表示登录用户名,series 和 tokenValue 则是自动生成的,date 表示上次使用时间。
PersistentTokenBasedRememberMeServices 里边重要的方法也是processAutoLoginCookie 和 onLoginSuccess,我们分别来看一下。
先来看 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) + "'");
}
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
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() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
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());
}
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,
HttpServletResponse response) {
setCookie(new String[] { token.getSeries(), token.getTokenValue() },
getTokenValiditySeconds(), request, response);
}
-
不同于 TokenBasedRememberMeServices 中的 processAutoLoginCookie 方法,这里 cookieTokens 数组的长度为 2,第一项是 series,第二项是token。
-
从 cookieTokens 数组中分别提取出 series 和 token,然后根据 series 去数据库中查询出一个 PersistentRememberMeToken 对象。如果查询出来的对象为 null,表示数据库中并没有 series 对应的值,本次自动登录失败;如果查询出来的 token 和从 cookieTokens 中解析出来的 token 不相同,说明自动登录令牌已经泄漏(恶意用户利用令牌登录后,数据库中的token 变了),此时移除当前用户的所有自动登录记录并抛出异常。
-
根据数据库中查询出来的结果判断令牌是否过期,如果过期就抛出异常。
-
生成一个新的 PersistentRememberMeToken 对象,用户名和 series 不变,token 重新生成,date 也使用当前时间。newToken 生成后,根据 series 去修改数据库中的 token 和 date(即每次自动登录后都会产生新的token 和 date)。
-
调用 addCookie 方法添加 Cookie,在 addCookie 方法中,会调用到我们前面所说的 setCookie 方法,但是要注意第一个数组参数中只有两项:series 和 token(即返回到前端的令是通过对 series 和 token 进行 Base64 编码得到的)。
-
最后将根据用户名香询用户对象并返回。
再来看登录成功的回调函数 onLoginSuccess:
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);
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
登录成功后,构建一个 PersistentRememberMeToken 对象,对象中的 series 和 token 参数都是随机生成的,然后将生成的对象存入数据库中,再调用 addCookie 方法添加相关的 Cookie 信息。
PersistentTokenBasedRememberMeServices 和 TokenBasedRememberMeServices 还是有些明显的区别的:前者返回给前端的令牌是将 series 和 token 组成的字符串进行 Base64 编码后返回给前端:后者返回给前端的令牌则是将用户名、过期时间以及签名组成的字符串进行 Base64 编码后返回给端。
那么 RememberMeServices 是在何时被调用的?这就要回到我们一开始的配置中了。
当开发者配置 .rememberMe().key("javaboy") 时,实际上是引入了配置类 RememberMeConfigurer,根据第 4 章的介绍,我们知道对于 RememberMeConfigurer 而言最重要的就是 init 和 configure 方法,我们先来看其 init 方法:
public void init(H http) throws Exception {
validateInput();
String key = getKey();
RememberMeServices rememberMeServices = getRememberMeServices(http, key);
http.setSharedObject(RememberMeServices.class, rememberMeServices);
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && this.logoutHandler != null) {
logoutConfigurer.addLogoutHandler(this.logoutHandler);
}
RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(
key);
authenticationProvider = postProcess(authenticationProvider);
http.authenticationProvider(authenticationProvider);
initDefaultLoginFilter(http);
}
在这里首先获取了一个 key,这个 key 就是开发者一开始配置的 key,如果没有配置,则会自动生成一个 UUID 字符串。如果开发者使用普通的 RememberMe,即没有使用持久化令牌,则建议开发者自行配置该 key,因为使用默认的 UUID 字符串,系统每次重启都会生成新的 key,会导致之前下发的 remember-me 失效。
有了 key 之后,接下来再去获取 RememberMeServices 实例,如果开发者配置了 tokenRepository,则获取到的 RememberMeServices 实例是 PersistentTokenBasedRememberMeServices,否则获取到 TokenBasedRememberMeServices,即系统通过有没有配置 tokenRepository 来确定使用哪种类型的 RememberMeServices。
同时,init 方法中还配置了一个 RememberMeAuthenticationProvider,该实例主要用来校验 key。
再来看 RememberMeConfigurer 的 configure 方法:
public void configure(H http) {
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter(
http.getSharedObject(AuthenticationManager.class),
this.rememberMeServices);
if (this.authenticationSuccessHandler != null) {
rememberMeFilter
.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
}
rememberMeFilter = postProcess(rememberMeFilter);
http.addFilter(rememberMeFilter);
}
configure 方法中主要创建了一个 RememberMeAuthenticationFilter,创建时传入 RememberMeServices 实例,最后将创建好的 RememberMeAuthenticationFilter 加入到过滤器链中。最后我们再来看一下 RememberMeAuthenticationFilter 中的 doFilter 是如何 “运筹惟” 的:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication()
+ "'");
}
// Fire event
if (this.eventPublisher != null) {
eventPublisher
.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext()
.getAuthentication(), this.getClass()));
}
if (successHandler != null) {
successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
return;
}
}
catch (AuthenticationException authenticationException) {
if (logger.isDebugEnabled()) {
logger.debug(
"SecurityContextHolder not populated with remember-me token, as "
+ "AuthenticationManager rejected Authentication returned by RememberMeServices: '"
+ rememberMeAuth
+ "'; invalidating remember-me token",
authenticationException);
}
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response,
authenticationException);
}
}
chain.doFilter(request, response);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
-
请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用 autoLogin 方法进行自动登录。
-
当自动登录成功后返回的 rememberMeAuth 不为 null 时,表示自动登录成功,此时调用 authenticate 方法对 key 进行校验,并且将登录成功的用户信息保存到 SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含 RememberMeServices 中的 loginSuccess 方法。
-
如果自动登录失败,则调用 rememberMeServices.loginFail 方法处理登录失败回调。onUnsuccessfulAuthentication 和 onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现。
这就是 RememberMeAuthenticationFilter 过滤器所做的事情,成功将 RememberMeServices 的服务集成进来。
最后再额外说一下 RememberMeServices#loginSuccess 方法的调用位置。该方法是在 AbstractAuthenticationProcessingFilter#successfulAuthentication 中触发的,也就是说,无论你是否开启了 RememberMe 功能,该方法都会被调用。只不过在 RememberMeServices#loginSuccess 方法的具体实现中,会去判断是否开启了 RememberMe,进而决定是否在响应中添加对应的 Cookie。
至此,整个 RememberMe 的用法还有原理就介绍完了。