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

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

概述

該過濾器的作用是處理過濾器鏈中發生的 AccessDeniedExceptionAuthenticationException 異常,將它們轉換成相應的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:認證過程中涉及到儲存請求時使用的請求快取策略,預設情況下是基於sessionHttpSessionRequestCache

如果你想觀察該過濾器的行為,可以在未登入狀態下訪問一個受登入保護的頁面,系統會丟擲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)) { 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 // 處理,預設告知其許可權不足 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(); } }); } } }

參考文章