希望CSDN能解决一个用户只能打开一个编辑器的问题,辛辛苦苦写的文字突然就没有了。
重新来过吧。
简单的说,Remember-me就是记住用户,下次登陆的时候不用密码也能登陆。实现这个功能主要是依靠cookie,因为Http是无状态协议,所以我们需要一个替服务端保存登陆状态的小饼干,这个小饼干就是cookie。
我们知道,用户初次登陆时候通过username、password完成认证(实际也有其他认证方式),那么让我们回忆登陆认证的过程。
1. 首先登陆的请求经过DelegatingFilterProxy传递到AbstractAuthenticationProcessingFilter
2. AbstractAuthenticationProcessingFilter传递给UsernamePasswordAuthenticationFilter
3. UsernamePasswordAuthenticationFilter把请求封装成Authentication对象,并调用ProviderManager的authenticate方法
4. ProviderManager把Authentication的认证工作交给具体的Provider(我们这里是DaoAuthenticationProvider)
5. DaoAuthenticationProvider调用具体的UserDetailsService查询出用户存储
6. 在整个过程中,假设认证失败则抛出异常,否则返回认证成功对象
7. AbstractAuthenticationProcessingFilter获取到下层传递过来的通过Authentication对象后将其封装成cookie并加载进response
在登陆的过程中,我们看到了Remember-me别涵盖在内。我们可以看到,在AbstractAuthenticationProcessingFilter调用了这样一个方法:
successfulAuthentication(request, response, chain, authResult);
进入这个方法,我们看到由如下调用:
//把Authentication对象存入安全上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
//调用RememberMeService的loginSuccess方法
rememberMeServices.loginSuccess(request, response, authResult);
继续深入看看这个方法:
onLoginSuccess(request, response, successfulAuthentication);
里面的方法体仅仅是调用了这个方法,这个方法有两个实现:
从名字上可以判断,一个是持久化的,一个是暂时的,我们看看暂时的那个。
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;
}
}
//获取token的活跃期限,用户设置
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
//加密成token串
String signatureValue = makeTokenSignature(expiryTime, username, password);
//生成cookie并设置到response,其中第一个参数是cookieValue,第二个参数时maxAge
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) + "'");
}
}
下面是token的加密算法:
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())));
}
下面是setCookie:
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request,
HttpServletResponse response) {
//用":"拼接字符串,并用base64加密
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);
}
if (setHttpOnlyMethod != null) {
ReflectionUtils.invokeMethod(setHttpOnlyMethod, cookie, Boolean.TRUE);
}
else if (logger.isDebugEnabled()) {
logger.debug("Note: Cookie will not be marked as HttpOnly because you are not using Servlet 3.0 (Cookie#setHttpOnly(boolean) was not found).");
}
response.addCookie(cookie);
}
至此,初次登陆的Remember-me逻辑理清楚了,不过这里有一个问题——有一条原本应当有的判断却消失了。一般情况是用户勾选了Remember-me才会开启Remember-me功能,但是在我们的源码分析里却找不到这句判断,这只能说明两种种可能性,一种可能性是Remember-me的判断在上一层做了,而我们或许正好就在下一层分析;另一种可能性是我们的分析从一开始就出错了。
从我的理解出发,Remember-me二次登陆的逻辑不应当被UsernamePasswordAuthenticationFilter处理,而应该专门被Remember-me的filter处理。
找了一下,看到了一个filter,它的名字是RememberMeAuthenticationFilter(该类位于org.springframework.security.web.authentication.rememberme),直接看看它的doFilter方法。其中有一句比较关键的如下:
Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);
继续深入探索。
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
//获取cookieValue
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 {
//解析cookieString
String[] cookieTokens = decodeCookie(rememberMeCookie);
//核心,通过cookie获取用户凭证
user = processAutoLoginCookie(cookieTokens, request, response);
//检验用户
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
//封装成Authentication对象
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;
}
下面是ProcessAutoLoginCookie:
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]).longValue();
}
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]);
// 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());
//校验token
if (!equals(expectedTokenSignature,cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
+ "' but expected '" + expectedTokenSignature + "'");
}
return userDetails;
}
可以看到验证成功后从AbstractRememberMeService中封装成Authentication返回到RememberMeAuthenticationFilter设置到安全上下文。
至此,整个Remember-me的过程大致是清楚了,期间产生的疑问只能等之后有能力更细致的解读的时候继续分析。
<security:http auto-config="true">
<security:intercept-url pattern="/**" access="hasROLE('USER')"/>
<security:form-login/>
<security:logout/>
<security:remember-me />
security:http>
可以直接这样添加该配置,此时访问页面要有一个勾选框,其id为_spring_security_remember_me
,当用户勾选该勾选框的时候就是启用了Remember-me功能了,这也就解释了为什么我们在分析源码的时候为什么没有判断是否开启Remember-me功能,这是因为判断的工作早在这里就完成了。
同时,顺便说一下之前有另一个实现,我们称之为持久化的那个实现,这两个实现有什么区别呢?事实上,通过上面的源码分析,与我印象的不同,它直接生成cookie,然后存放到客户端,之后每次登陆就把这个cookie当做登陆凭证,那么意味着能否登陆由客户端决定,服务端是没有备份的。与之相反,持久化的cookie策略是有备份到数据库的,这样每次登陆的时候就要比对一下客户端的cookie与服务端的cookie是否一致,相应的,在配置的时候就要配置DataSource。