一文搞懂Spring?Security異常處理機制
今天來和小伙伴們聊一聊 Spring Security 中的異常處理機制。
在 Spring Security 的過濾器鏈中,ExceptionTranslationFilter 過濾器專門用來處理異常,在 ExceptionTranslationFilter 中,我們可以看到,異常被分為了兩大類:認證異常和授權(quán)異常,兩種異常分別由不同的回調(diào)函數(shù)來處理,今天就來和大家分享一下這里的條條框框。
1.異常分類
Spring Security 中的異常可以分為兩大類,一種是認證異常,一種是授權(quán)異常。
認證異常就是 AuthenticationException,它有眾多的實現(xiàn)類:

可以看到,這里的異常實現(xiàn)類還是蠻多的,都是都是認證相關(guān)的異常,也就是登錄失敗的異常。這些異常,有的松哥在之前的文章中都和大家介紹過了,例如下面這段代碼
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanceof LockedException) {
respBean.setMsg("賬戶被鎖定,請聯(lián)系管理員!");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密碼過期,請聯(lián)系管理員!");
} else if (e instanceof AccountExpiredException) {
respBean.setMsg("賬戶過期,請聯(lián)系管理員!");
} else if (e instanceof DisabledException) {
respBean.setMsg("賬戶被禁用,請聯(lián)系管理員!");
} else if (e instanceof BadCredentialsException) {
respBean.setMsg("用戶名或者密碼輸入錯誤,請重新輸入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
另一類就是授權(quán)異常 AccessDeniedException,授權(quán)異常的實現(xiàn)類比較少,因為授權(quán)失敗的可能原因比較少。

2.ExceptionTranslationFilter
ExceptionTranslationFilter 是 Spring Security 中專門負責處理異常的過濾器,默認情況下,這個過濾器已經(jīng)被自動加載到過濾器鏈中。
有的小伙伴可能不清楚是怎么被加載的,我這里和大家稍微說一下。
當我們使用 Spring Security 的時候,如果需要自定義實現(xiàn)邏輯,都是繼承自 WebSecurityConfigurerAdapter 進行擴展,WebSecurityConfigurerAdapter 中本身就進行了一部分的初始化操作,我們來看下它里邊 HttpSecurity 的初始化過程:
protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
}
AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManager);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
configure(http);
return http;
}
可以看到,在 getHttp 方法的最后,調(diào)用了 configure(http);,我們在使用 Spring Security 時,自定義配置類繼承自 WebSecurityConfigurerAdapter 并重寫的 configure(HttpSecurity http) 方法就是在這里調(diào)用的,換句話說,當我們?nèi)ヅ渲?HttpSecurity 時,其實它已經(jīng)完成了一波初始化了。
在默認的 HttpSecurity 初始化的過程中,調(diào)用了 exceptionHandling 方法,這個方法會將 ExceptionHandlingConfigurer 配置進來,最終調(diào)用 ExceptionHandlingConfigurer#configure 方法將 ExceptionTranslationFilter 添加到 Spring Security 過濾器鏈中。
我們來看下 ExceptionHandlingConfigurer#configure 方法源碼:
@Override
public void configure(H http) {
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
entryPoint, getRequestCache(http));
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
可以看到,這里構(gòu)造了兩個對象傳入到 ExceptionTranslationFilter 中:
- AuthenticationEntryPoint 這個用來處理認證異常。
- AccessDeniedHandler 這個用來處理授權(quán)異常。
具體的處理邏輯則在 ExceptionTranslationFilter 中,我們來看一下:
public class ExceptionTranslationFilter extends GenericFilterBean {
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
RequestCache requestCache) {
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, ase);
}
else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
}
ExceptionTranslationFilter 的源碼比較長,我這里列出來核心的部分和大家分析:
- 過濾器最核心的當然是 doFilter 方法,我們就從 doFilter 方法看起。這里的 doFilter 方法中過濾器鏈繼續(xù)向下執(zhí)行,ExceptionTranslationFilter 處于 Spring Security 過濾器鏈的倒數(shù)第二個,最后一個是 FilterSecurityInterceptor,F(xiàn)ilterSecurityInterceptor 專門處理授權(quán)問題,在處理授權(quán)問題時,就會發(fā)現(xiàn)用戶未登錄、未授權(quán)等,進而拋出異常,拋出的異常,最終會被 ExceptionTranslationFilter#doFilter 方法捕獲。
- 當捕獲到異常之后,接下來通過調(diào)用
throwableAnalyzer.getFirstThrowableOfType方法來判斷是認證異常還是授權(quán)異常,判斷出異常類型之后,進入到 handleSpringSecurityException 方法進行處理;如果不是 Spring Security 中的異常類型,則走 ServletException 異常類型的處理邏輯。 - 進入到 handleSpringSecurityException 方法之后,還是根據(jù)異常類型判斷,如果是認證相關(guān)的異常,就走 sendStartAuthentication 方法,最終被 authenticationEntryPoint.commence 方法處理;如果是授權(quán)相關(guān)的異常,就走 accessDeniedHandler.handle 方法進行處理。
AuthenticationEntryPoint 的默認實現(xiàn)類是 LoginUrlAuthenticationEntryPoint,因此默認的認證異常處理邏輯就是 LoginUrlAuthenticationEntryPoint#commence 方法,如下:
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
可以看到,就是重定向,重定向到登錄頁面(即當我們未登錄就去訪問一個需要登錄才能訪問的資源時,會自動重定向到登錄頁面)。
AccessDeniedHandler 的默認實現(xiàn)類則是 AccessDeniedHandlerImpl,所以授權(quán)異常默認是在 AccessDeniedHandlerImpl#handle 方法中處理的:
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
if (errorPage != null) {
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
response.setStatus(HttpStatus.FORBIDDEN.value());
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else {
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
}
可以看到,這里就是服務(wù)端跳轉(zhuǎn)返回 403。
3.自定義處理
前面和大家介紹了 Spring Security 中默認的處理邏輯,實際開發(fā)中,我們可以需要做一些調(diào)整,很簡單,在 exceptionHandling 上進行配置即可。
首先自定義認證異常處理類和授權(quán)異常處理類:
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.getWriter().write("login failed:" + authException.getMessage());
}
}
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(403);
response.getWriter().write("Forbidden:" + accessDeniedException.getMessage());
}
}
然后在 SecurityConfig 中進行配置,如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
...
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
.and()
...
...
}
}
配置完成后,重啟項目,認證異常和授權(quán)異常就會走我們自定義的邏輯了。
到此這篇關(guān)于一文搞懂Spring Security異常處理機制的文章就介紹到這了,更多相關(guān)Spring Security異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Javaweb開發(fā)環(huán)境Myeclipse6.5 JDK1.6 Tomcat6.0 SVN1.8配置教程
這篇文章主要介紹了Javaweb開發(fā)環(huán)境Myeclipse6.5 JDK1.6 Tomcat6.0 SVN1.8配置教程,感興趣的小伙伴們可以參考一下2016-06-06
java使用this調(diào)用構(gòu)造函數(shù)的實現(xiàn)方法示例
這篇文章主要介紹了java使用this調(diào)用構(gòu)造函數(shù)的實現(xiàn)方法,結(jié)合實例形式分析了java面向?qū)ο蟪绦蛟O(shè)計中函數(shù)調(diào)用相關(guān)操作技巧,需要的朋友可以參考下2019-08-08

