Spring Security Web 5.1.2 原始碼解析 -- ExceptionTranslationFilter
阿新 • • 發佈:2018-12-12
概述
該過濾器的作用是處理過濾器鏈中發生的 AccessDeniedException
和 AuthenticationException
異常,將它們轉換成相應的HTTP
響應。
當檢測到 AuthenticationException
異常時,該過濾器會啟動 authenticationEntryPoint
,也就是啟動認證流程。
當檢測到 AccessDeniedException
異常時,該過濾器先判斷當前使用者是否為匿名訪問或者Remember Me
訪問。如果是這兩種情況之一,會啟動 authenticationEntryPoint
邏輯。如果安全配置開啟了使用者名稱/密碼錶單認證,通常這個authenticationEntryPoint
LoginUrlAuthenticationEntryPoint
。它執行時會將使用者帶到登入頁面,開啟登入認證流程。
如果不是匿名訪問或者Remember Me
訪問,接下來的處理會交給一個 AccessDeniedHandler
來完成。預設情況下,這個 AccessDeniedHandler
的實現類是 AccessDeniedHandlerImpl
,它會:
- 請求新增HTTP 403異常屬性,記錄相應的異常;
- 然後往寫入響應HTTP狀態碼403;
- 並
foward
到相應的錯誤頁面。
使用該過濾器必須要設定以下屬性:
authenticationEntryPoint
handler
)- 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)) {
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();
}
});
}
}
}