天天看點

Spring Security Web 5.1.2 源碼解析 -- ExceptionTranslationFilter概述源代碼解析參考文章

概述

該過濾器的作用是處理過濾器鍊中發生的

AccessDeniedException

AuthenticationException

異常,将它們轉換成相應的

HTTP

響應。

當檢測到

AuthenticationException

異常時,該過濾器會啟動

authenticationEntryPoint

,也就是啟動認證流程。

當檢測到

AccessDeniedException

異常時,該過濾器先判斷目前使用者是否為匿名通路或者

Remember Me

通路。如果是這兩種情況之一,會啟動

authenticationEntryPoint

邏輯。如果安全配置開啟了使用者名/密碼表單認證,通常這個

authenticationEntryPoint

會對應到一個

LoginUrlAuthenticationEntryPoint

。它執行時會将使用者帶到登入頁面,開啟登入認證流程。

如果不是匿名通路或者

Remember Me

通路,接下來的處理會交給一個

AccessDeniedHandler

來完成。預設情況下,這個

AccessDeniedHandler

的實作類是

AccessDeniedHandlerImpl

,它會:

  1. 請求添加

    HTTP 403

    異常屬性,記錄相應的異常;
  2. 然後往寫入響應HTTP狀态碼

    403

    ;
  3. foward

    到相應的錯誤頁面。

使用該過濾器必須要設定以下屬性:

  1. authenticationEntryPoint

    :用于啟動認證流程的處理器(

    handler

    )
  2. requestCache:認證過程中涉及到儲存請求時使用的請求緩存政策,預設情況下是基于

    session

    HttpSessionRequestCache

如果你想觀察該過濾器的行為,可以在未登入狀态下通路一個受登入保護的頁面,系統會抛出

AccessDeniedException

并最終進入該

Filter

的職責流程。

源代碼解析

package org.springframework.security.web.access;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.security.web.util.ThrowableCauseExtractor;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

import org.springframework.context.support.MessageSourceAccessor;

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 java.io.IOException;


public class ExceptionTranslationFilter extends GenericFilterBean {

	// ~ Instance fields
	// =====================================================================================
	
	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
	private AuthenticationEntryPoint authenticationEntryPoint;
	// 用于判斷一個Authentication是否Anonymous,Remember Me,
	// 預設使用 AuthenticationTrustResolverImpl
	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
	// 用于分析一個Throwable抛出的原因,使用本類自定義的嵌套類DefaultThrowableAnalyzer,
	// 主要是加入了對ServletException的分析
	private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

	// 請求緩存,預設使用HttpSessionRequestCache,在遇到異常啟動認證過程時會用到,
	// 因為要先把原始請求緩存下來,一旦認證成功結果,需要把原始請求提出重新跳轉到相應URL
	private RequestCache requestCache = new HttpSessionRequestCache();

	private final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
		this(authenticationEntryPoint, new HttpSessionRequestCache());
	}

	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
			RequestCache requestCache) {
		Assert.notNull(authenticationEntryPoint,
				"authenticationEntryPoint cannot be null");
		Assert.notNull(requestCache, "requestCache cannot be null");
		this.authenticationEntryPoint = authenticationEntryPoint;
		this.requestCache = requestCache;
	}

	// ~ Methods
	// ====================================================================================

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(authenticationEntryPoint,
				"authenticationEntryPoint must be specified");
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		// 在任何請求到達時不做任何操作,直接放行,繼續filter chain的執行,
		// 但是使用一個 try-catch 來捕獲filter chain中接下來會發生的各種異常,
		// 重點關注其中的以下異常,其他異常繼續向外抛出 :
		// AuthenticationException : 認證失敗異常,通常因為認證資訊錯誤導緻
		// AccessDeniedException : 通路被拒絕異常,通常因為權限不足導緻
		try {
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			// 檢測ex是否由AuthenticationException或者AccessDeniedException異常導緻
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				if (response.isCommitted()) {
				
				// 如果response已經送出,則沒辦法向響應中轉換和寫入這些異常了,隻好抛一個異常
				throw new ServletException(
		"Unable to handle the Spring Security Exception because the response is already committed.", 
				ex);
			
				}
				// 如果ex是由AuthenticationException或者AccessDeniedException異常導緻,
				// 并且響應尚未送出,這裡将這些Spring Security異常翻譯成相應的 http response。
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}

	public AuthenticationEntryPoint getAuthenticationEntryPoint() {
		return authenticationEntryPoint;
	}

	protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
		return authenticationTrustResolver;
	}

	// 此方法僅用于處理兩種Spring Security 異常:
	// AuthenticationException , AccessDeniedException
	private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			logger.debug(
				"Authentication exception occurred; redirecting to authentication entry point",
				exception);

			// 如果是 AuthenticationException 異常,啟動認證流程
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {		
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			
			if (authenticationTrustResolver.isAnonymous(authentication) || 
				authenticationTrustResolver.isRememberMe(authentication)) {
			// 如果目前認證token是匿名或者RememberMe token				
				
			logger.debug(
			"Access is denied (user is " + 
			(authenticationTrustResolver.isAnonymous(authentication) 
			? "anonymous" 
			: "not fully authenticated") + "); redirecting to authentication entry point",
			exception);

			// 如果是 AccessDeniedException 異常,而且目前登入主體是匿名狀态或者
			// Remember Me認證,則也啟動認證流程
			sendStartAuthentication(
				request,
				response,
				chain,
				new InsufficientAuthenticationException(
				messages.getMessage(
						"ExceptionTranslationFilter.insufficientAuthentication",
						"Full authentication is required to access this resource")));
			}
			else {
				logger.debug(
				"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
				exception);

				// 如果是 AccessDeniedException 異常,而且目前使用者不是匿名,也不是
				// Remember Me, 而是真正經過認證的某個使用者,則說明是該使用者權限不足,
				// 則交給 accessDeniedHandler 處理,預設告知其權限不足
				// 注意 : 如果目前被請求的頁面被配置成RememberMe權限可通路,但實際上
				// 目前目前安全上下文中的token是fullAuthenticated的,則也會走到這個流程
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}

	// 發起認證流程
	protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// SEC-112: Clear the SecurityContextHolder's Authentication, as the
		// existing Authentication is no longer considered valid

		// 将SecurityContextHolder中SecurityContext的authentication設定為null
		SecurityContextHolder.getContext().setAuthentication(null);

		// 儲存目前請求,一旦認證成功,認證機制會再次提取該請求并跳轉到該請求對應的頁面
		requestCache.saveRequest(request, response);

		// 準備工作已經做完,開始認證流程
		logger.debug("Calling Authentication entry point.");
		authenticationEntryPoint.commence(request, response, reason);
	}

	public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
		Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
		this.accessDeniedHandler = accessDeniedHandler;
	}

	public void setAuthenticationTrustResolver(
			AuthenticationTrustResolver authenticationTrustResolver) {
		Assert.notNull(authenticationTrustResolver,
				"authenticationTrustResolver must not be null");
		this.authenticationTrustResolver = authenticationTrustResolver;
	}

	public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
		Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
		this.throwableAnalyzer = throwableAnalyzer;
	}

	/**
	 * Default implementation of ThrowableAnalyzer which is capable of also
	 * unwrapping ServletExceptions.
	 */
	private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
	
		protected void initExtractorMap() {
			super.initExtractorMap();

			registerExtractor(ServletException.class, new ThrowableCauseExtractor() {
				public Throwable extractCause(Throwable throwable) {
					ThrowableAnalyzer.verifyThrowableHierarchy(throwable,
							ServletException.class);
					return ((ServletException) throwable).getRootCause();
				}
			});
		}

	}

}
           

參考文章

  • Spring Security Web 5.1.2 源碼解析 – 安全相關Filter清單