Spring Security Web 5.1.2 原始碼解析 -- RememberMeAuthenticationFilter
阿新 • • 發佈:2018-12-12
概述
預設情況下,如果安全配置開啟了Remember Me
機制,使用者在登入介面上會看到Remember Me
選擇框,如果使用者選擇了該選擇框,會導致生成一個名為remember-me
,屬性httpOnly
為true
的cookie
,其值是一個RememberMe token
。
RememberMe token
是一個Base64
編碼的字串,解碼後格式為{使用者名稱}:{Token過期時間戳}:{Token簽名摘要}
,比如:admin:1545787408479:d0b0e7a53960e94b521bee3f02ba0bf5
而該過濾器在每次請求到達時會檢測SecurityContext
Authentication
是否已經設定。如果沒有設定,會進入該過濾器的職責邏輯。它嘗試獲取名為remember-me
的cookie
,獲取到的話會認為這是一次Remember Me
登入嘗試,從中分析出使用者名稱,Token
過期時間戳,簽名摘要,針對使用者庫驗證這些資訊,認證通過的話,就會往SecurityContext
裡面設定Authentication
為一個針對請求中所指定使用者的RememberMeAuthenticationToken
。
認證成功的話,也會嚮應用上下文釋出事件InteractiveAuthenticationSuccessEvent
。
預設情況下不管認證成功還是失敗,請求都會被繼續執行。
不過也可以指定一個
AuthenticationSuccessHandler
給當前過濾器,這樣當Remember Me
登入認證成功時,處理委託給該AuthenticationSuccessHandler
,而不再繼續原請求的處理。利用這種機制,可以為Remember Me
登入認證成功指定特定的跳轉地址。
Remember Me
登入認證成功並不代表使用者一定可以訪問到目標頁面,因為如果Remember Me
登陸認證成功對應使用者訪問許可權級別為isRememberMe
,而目標頁面有可能需要更高的訪問許可權級別fullyAuthenticated
。
如果你想觀察該過濾器的行為,可以這麼做:
- 在配置中開啟
Remember Me
機制,則此過濾器會被使用;- 啟動應用,開啟瀏覽器,提供正確的使用者名稱密碼,選擇
Remember Me
選項,然後提交完成一次成功的登入;- 關閉整個瀏覽器;
- 重新開啟剛剛關閉的瀏覽器;
- 直接訪問某個受登入保護的頁面,你會看到該過濾器的職責邏輯被執行。
原始碼解析
package org.springframework.security.web.authentication.rememberme;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
public class RememberMeAuthenticationFilter extends GenericFilterBean implements
ApplicationEventPublisherAware {
// ~ Instance fields
// =======================================================================================
private ApplicationEventPublisher eventPublisher;
private AuthenticationSuccessHandler successHandler;
private AuthenticationManager authenticationManager;
private RememberMeServices rememberMeServices;
public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager,
RememberMeServices rememberMeServices) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.authenticationManager = authenticationManager;
this.rememberMeServices = rememberMeServices;
}
// ~ Methods
// =====================================================================================
@Override
public void afterPropertiesSet() {
Assert.notNull(authenticationManager, "authenticationManager must be specified");
Assert.notNull(rememberMeServices, "rememberMeServices must be specified");
}
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) {
// 如果SecurityContext中authentication為空則嘗試 remember me 自動認證,
// 預設情況下這裡rememberMeServices會是一個TokenBasedRememberMeServices,
// 其自動 remember me 認證過程如下:
// 1. 獲取 cookie remember-me 的值 , 一個base64 編碼串;
// 2. 從上面cookie之中解析出資訊:使用者名稱,token 過期時間,token 簽名
// 3. 檢查使用者是否存在,token是否過期,token 簽名是否一致,
// 上面三個步驟都通過的情況下再檢查一下賬號是否鎖定,過期,禁用,密碼過期等現象,
// 如果上面這些驗證都通過,則認為認證成功,會構造一個
// RememberMeAuthenticationToken並返回
// 上面的認證失敗會有rememberMeAuth==null
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
// 如果上面的 Remember Me 認證成功,則需要使用 authenticationManager
// 認證該rememberMeAuth
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
// 將認證成功的rememberMeAuth放到SecurityContextHolder中的SecurityContext
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) {
// 釋出事件 InteractiveAuthenticationSuccessEvent 到應用上下文
eventPublisher
.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext()
.getAuthentication(), this.getClass()));
}
if (successHandler != null) {
// 如果指定了 successHandler ,則呼叫它,
// 預設情況下這個 successHandler 為 null
successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
// 如果指定了 successHandler,在它呼叫之後,不再繼續 filter chain 的執行
return;
}
}
catch (AuthenticationException authenticationException) {
// Remember Me 認證失敗的情況
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 的認證失敗處理
rememberMeServices.loginFail(request, response);
// 空方法,這裡什麼都不做
onUnsuccessfulAuthentication(request, response,
authenticationException);
}
}
// 繼續 filter chain 執行
chain.doFilter(request, response);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
// 繼續 filter chain 執行
chain.doFilter(request, response);
}
}
/**
* Called if a remember-me token is presented and successfully authenticated by the
* RememberMeServices autoLogin method and the
* AuthenticationManager.
*/
protected void onSuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, Authentication authResult) {
}
/**
* Called if the AuthenticationManager rejects the authentication object
* returned from the RememberMeServices autoLogin method. This method
* will not be called when no remember-me token is present in the request and
* autoLogin reurns null.
*/
protected void onUnsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) {
}
public RememberMeServices getRememberMeServices() {
return rememberMeServices;
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* Allows control over the destination a remembered user is sent to when they are
* successfully authenticated. By default, the filter will just allow the current
* request to proceed, but if an AuthenticationSuccessHandler is set, it will
* be invoked and the doFilter() method will return immediately, thus allowing
* the application to redirect the user to a specific URL, regardless of whatthe
* original request was for.
* 預設情況下,Remember Me 登入認證成功時filter chain會繼續執行。但是也允許指定一個
* AuthenticationSuccessHandler , 這樣就可以控制 Remember Me 登入認證成功時的目標
* 跳轉地址(當然會忽略原始的請求目標)。
* @param successHandler the strategy to invoke immediately before returning from
* doFilter().
*/
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
}