1. 程式人生 > >Spring Security Web 5.1.2 原始碼解析 -- RememberMeAuthenticationFilter

Spring Security Web 5.1.2 原始碼解析 -- RememberMeAuthenticationFilter

概述

預設情況下,如果安全配置開啟了Remember Me機制,使用者在登入介面上會看到Remember Me選擇框,如果使用者選擇了該選擇框,會導致生成一個名為remember-me,屬性httpOnlytruecookie,其值是一個RememberMe token

RememberMe token是一個Base64編碼的字串,解碼後格式為{使用者名稱}:{Token過期時間戳}:{Token簽名摘要},比如:admin:1545787408479:d0b0e7a53960e94b521bee3f02ba0bf5

而該過濾器在每次請求到達時會檢測SecurityContext

屬性Authentication是否已經設定。如果沒有設定,會進入該過濾器的職責邏輯。它嘗試獲取名為remember-mecookie,獲取到的話會認為這是一次Remember Me登入嘗試,從中分析出使用者名稱,Token過期時間戳,簽名摘要,針對使用者庫驗證這些資訊,認證通過的話,就會往SecurityContext裡面設定Authentication為一個針對請求中所指定使用者的RememberMeAuthenticationToken

認證成功的話,也會嚮應用上下文釋出事件InteractiveAuthenticationSuccessEvent

預設情況下不管認證成功還是失敗,請求都會被繼續執行。

不過也可以指定一個AuthenticationSuccessHandler給當前過濾器,這樣當Remember Me登入認證成功時,處理委託給該AuthenticationSuccessHandler,而不再繼續原請求的處理。利用這種機制,可以為Remember Me登入認證成功指定特定的跳轉地址。

Remember Me登入認證成功並不代表使用者一定可以訪問到目標頁面,因為如果Remember Me登陸認證成功對應使用者訪問許可權級別為isRememberMe,而目標頁面有可能需要更高的訪問許可權級別fullyAuthenticated

如果你想觀察該過濾器的行為,可以這麼做:

  1. 在配置中開啟Remember Me機制,則此過濾器會被使用;
  2. 啟動應用,開啟瀏覽器,提供正確的使用者名稱密碼,選擇Remember Me選項,然後提交完成一次成功的登入;
  3. 關閉整個瀏覽器;
  4. 重新開啟剛剛關閉的瀏覽器;
  5. 直接訪問某個受登入保護的頁面,你會看到該過濾器的職責邏輯被執行。

原始碼解析

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;
	}

}

相關文章

參考文章