RememberMeAuthenticationFilter
Remember Me的功能为记住用户的登录状态并保持一定时间。在此期间内,即便是用户session已经失效,用户仍然可以免登录访问系统。
RememberMeAuthenticationFilter
主要功能为实现这一逻辑,实现自动登陆功能。
我们分析下它的doFilter
方法,内容如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 如果已经有认证信息,跳过此filter
if (SecurityContextHolder.getContext().getAuthentication() != null) {
this.logger.debug(LogMessage
.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
chain.doFilter(request, response);
return;
}
// 使用RememberMe服务自动登陆
// autoLogin方法的逻辑在下一节介绍
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
// 如果获取到了authentication
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
// 使用AuthenticationManager认证
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
// 储存认证信息到SecurityContext
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
// 发布认证成功事件
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
// 调用登陆成功handler的相关逻辑
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
}
catch (AuthenticationException ex) {
// 认证失败,调用失败逻辑
this.logger.debug(LogMessage
.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
+ "rejected Authentication returned by RememberMeServices: '%s'; "
+ "invalidating remember-me token", rememberMeAuth),
ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
}
}
chain.doFilter(request, response);
}
AbstractRememberMeServices
Remember me相关的service封装了主要的实现逻辑,包含如何从remember me cookie中获取到用户信息和登陆状态,检查cookie内容的有效性和如何在用户允许时启用remember me功能。
autoLogin
方法的功能为从cookie中获取用户信息并认证。它的内容如下:
@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
// 获取remeber me服务保存信息的cookie,默认cookie名称为remember-me
String rememberMeCookie = extractRememberMeCookie(request);
// 如果没找到cookie,返回
if (rememberMeCookie == null) {
return null;
}
this.logger.debug("Remember-me cookie detected");
// 如果cookie value长度为0
// 让这个cookie立刻过期
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
try {
// 解析cookie获取token
// base64解码,以冒号为分隔符切分string为数组后再依次URL解码
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 获取user详细信息,这个方法在子类中实现
UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
// 检查user信息
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
// 创建认证成功的authentication
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException ex) {
cancelCookie(request, response);
throw ex;
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
}
catch (InvalidCookieException ex) {
this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
}
catch (AccountStatusException ex) {
this.logger.debug("Invalid UserDetails: " + ex.getMessage());
}
catch (RememberMeAuthenticationException ex) {
this.logger.debug(ex.getMessage());
}
cancelCookie(request, response);
return null;
}
createSuccessfulAuthentication
方法,将用户认证信息包装为RememberMeAuthenticationToken
。
注意,remember me服务包含有一个key
,在这里会被写入token。Token在校验的时候会拿出它的key,同remember me服务的key做比较,只有key相同才能认证通过。
protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
return auth;
}
loginSuccess
方法负责检查request中是否带有remeber me参数(勾选了“记住我”复选框)或者是配置了永远启用remember me。如果启用了remember me,调用子类的onLoginSuccess
方法。AbstractAuthenticationProcessingFilter
和BasicAuthenticationFilter
中会调用该方法。
@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
// rememberMeRequested方法判断是否需要启用remember me服务
// this.parameter为remember me的参数名,默认为remember-me
if (!rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
rememberMeRequested
方法包含了用来判断是否启用remember me服务(用户是否勾选remember me复选框)的具体操作。内容如下:
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
// 如果永远启用,返回true
if (this.alwaysRemember) {
return true;
}
// 获取remember me请求参数的值
String paramValue = request.getParameter(parameter);
// 如果是true,on,yes或1,返回true
if (paramValue != null) {
if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
return true;
}
}
this.logger.debug(
LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
return false;
}
到这里AbstractRememberMeServices
抽象类的主要逻辑已分析完毕。
下面我们重点分析下AbstractRememberMeServices
两个子类:
- TokenBasedRememberMeServices
- PersistentTokenBasedRememberMeServices
的onLoginSuccess
方法和processAutoLoginCookie
方法。
其中onLoginSuccess
方法负责在认证成功时,将用户的登陆状态记录下来,从而做到有效期内免登录。
processAutoLoginCookie
负责从cookie中读取用户的登陆状态,还原为用户详细信息(UserDetails)。
TokenBasedRememberMeServices
该类将用户认证token相关信息保存在cookie中,不需要数据库等外部存储。
onLoginSuccess
方法内容如下:
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
// 从authentication获取用户名和密码
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)) {
this.logger.debug("Unable to retrieve username");
return;
}
// 如果密码找不到,从UserDetailsService中查找用户的密码
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
// 如果还是找不到密码,忽略后面的逻辑
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}
// 获取token有效时间
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
// 计算token过期的时间戳,如果token有效时间小于0,设置过期时间为2周后
expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
// 计算token签名
// token签名为MD5(username + ":" + tokenExpiryTime + ":" + password + ":" + getKey())
String signatureValue = makeTokenSignature(expiryTime, username, password);
// 设置token内容到cookie
// Cookie内容为Base64(URLEncode(username):URLEncode(expiryTime):URLEncode(signatureValue))
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}
processAutoLoginCookie
方法:
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
// 依照上面的分析,token解析前是一个string数组,长度为3
// 如果这里长度不为3,说明token有误,抛出异常
if (cookieTokens.length != 3) {
throw new InvalidCookieException(
"Cookie token did not contain 3" + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
// 获取token过期时间,位于string数组第二个元素
long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
// 如果token已过期(小于系统当前时间),抛出异常
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.
// 检查cookie中保存的用户名对应的用户是否存在
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.
// 重新计算token的签名,判断是否和cookie中保存的一致
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;
}
PersistentTokenBasedRememberMeServices
该实现方式把token的内容写入到tokenRepository
中,支持token内容持久化。可以防御cookie窃取攻击。
onLoginSuccess
方法:
@Override
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
// 参数中的序列号和token数据均为Base64编码的随机字节数据
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
generateTokenData(), new Date());
try {
// 保存token
this.tokenRepository.createNewToken(persistentToken);
// 设置token到cookie中
// cookie内容为Base64(URLEncode(序列号):URLEncode(token数据))
addCookie(persistentToken, request, response);
}
catch (Exception ex) {
this.logger.error("Failed to save persistent token ", ex);
}
}
PersistentTokenRepository
负责保存持久化的token信息。有两个子类:
- InMemoryTokenRepositoryImpl:保存token信息到内存中
- JdbcTokenRepositoryImpl:保存token信息到数据库
processAutoLoginCookie
方法:
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
// 按照上面的分析,persistence类型的cookie内容包含2部分
// 如果长度不为2,cookie内容无效,抛出异常
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '"
+ Arrays.asList(cookieTokens) + "'");
}
// 获取序列号和token数据
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
// 从tokenRepository中读取PersistentRememberMeToken
PersistentRememberMeToken token = this.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数据和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中保存的用户token
this.tokenRepository.removeUserTokens(token.getUsername());
// 抛出异常,很可能遭受到cookie窃取攻击
throw new CookieTheftException(this.messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
// 判断token是否过期
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.
this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'",
token.getUsername(), token.getSeries()));
// 认证成功后,需要更新token数据,但是保持序列号内容不变
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
generateTokenData(), new Date());
try {
// 更新token的内容
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
// 重新设置token内容到cookie中
addCookie(newToken, request, response);
}
catch (Exception ex) {
this.logger.error("Failed to update token: ", ex);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
// 从UserDetailsService中读取UserDetails并返回
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
本文为原创内容,欢迎大家讨论、批评指正与转载。转载时请注明出处。
网友评论