Spring Security 是根据cookie来完成自动登录功能的,所以我将解析分为两部分:何时让请求带上cookie;如何去解析cookie。
1、何时添加cookie
还记得处理登录的过滤器嘛,没错,一切都是从哪里开始,AbstractAuthenticationProcessingFilter。我们先去扎到它的doFilter()方法,如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 1. 第一部分
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 2. 第二部分
successfulAuthentication(request, response, chain, authenticationResult);
}
// 3. 第三部分
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
我们能很简单地将它所要实现的功能分为三部分:①:认证过程;②认证成功处理;③:认证失败、报错处理。
我们顺着第二部分走下去:
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
// 1. 第一部分
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 2. 第二部分
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
我们又能将该方法所要实现的功能分为两部分:①:rememberServices;②:成功处理措施
我们从第一部分走,我们所要寻找的接口为RememberMeServices
,默认实现为NullRememberMeServices
。看了下接口,我们发现如果有使用remember功能,那使用的实现类为TokenBasedRememberMeServices
。
顺着往下找loginSuccess()方法:
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
if (!rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
return;
}
// 调用下面的onLoginSuccess()方法
onLoginSuccess(request, response, successfulAuthentication);
}
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
// 1. 获取用户名和密码
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// 2. 用户、密码校验
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
return;
}
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;
}
}
// 3. 生成cookie并设置到请求中
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);
}
我们又将上面方法的流程分为三步:
- 获取用户名和密码
- 对用户名校验是否为空;对密码校验是否被擦除,如果是,则重新获取
- 用户名、密码、过期时间传入方法生成cookie并在请求上设置
- cookie的组成有用户名、密码、过期时间、key组成,采用MD5加密方式,其中key为一个UUID字符串,每次程序重启都会改变,建议设置
登录流程的记住我功能就此结束,下面看看如何进行验证
2、如何去解析cookie,实现自动登录
Spring Security中也有一个过滤器来实现自动登录的功能,那就是RememberMeAuthenticationFilter
,go,我们直接去看它的doIFilter()方法。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 查看有没有Authentication
if (SecurityContextHolder.getContext().getAuthentication() != null) {
chain.doFilter(request, response);
return;
}
// 2. 解析cookie
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
// 3. 验证Authentication
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
// 4. 成功后的操作
onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(
new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext()
.getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
}
// 5. 失败后的操作
catch (AuthenticationException ex) {
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
}
}
chain.doFilter(request, response);
}
我们可以将上面的方法实现分为5个部分:
- 查看是否需要解析cookie
- 解析cookie
- 验证Authentication
- 验证成功后的操作
- 验证失败后的操作
我们就主要看下,cookie是怎么解析出来的
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
// 1. 根据remember-me找到相应的cookie
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
if (rememberMeCookie.length() == 0) {
cancelCookie(request, response);
return null;
}
try {
// 2. 解码cookie
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 3. 验证解码出来的信息
UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
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;
}
上面方法主要实现三个:
- 获取cookie并验证它
- 解码cookie
- 拿解码出来的信息去核对,也就是调用loadUserByUsername()方法。
就这样,很简单!!