SpringSecurity异常处理源码解析
异常处理主要是由ExceptionTranslationFilter
完成的,而ExceptionTranslationFilter
是由ExceptionHandlingConfigurer
配置的,所以我们从ExceptionHandlingConfigurer
开始解析
我们首先看ExceptionHandlingConfigurer
的configure(Httpsecurity security)
方法:
@Override
public void configure(H http) {
//获取AuthenticationEntryPoint
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);//1
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
getRequestCache(http));//2
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);//3
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler//4
http.addFilter(exceptionTranslationFilter);//5
}
这里
AuthenticationEntryPoint
是配置的主要类所以我们来解析
getAuthenticationEntryPoint(http)
方法:AuthenticationEntryPoint getAuthenticationEntryPoint(H http) { AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;//1 if (entryPoint == null) {//2 entryPoint = createDefaultEntryPoint(http);//3 } return entryPoint; }
- 获取配置的
authenticationEntryPoint
- 判断是否配置了
authenticationEntryPoint
- 创建默认的
EntryPoint
我们继续看上面的
createDefaultEntryPoint
方法private AuthenticationEntryPoint createDefaultEntryPoint(H http) { if (this.defaultEntryPointMappings.isEmpty()) { return new Http403ForbiddenEntryPoint();//1 } if (this.defaultEntryPointMappings.size() == 1) { return this.defaultEntryPointMappings.values().iterator().next();//2 } DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint( this.defaultEntryPointMappings); entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next()); return entryPoint; }
- 如果没有设置默认的
EntryPoint
则创建一个Http403ForbiddenEntryPoint
,该Http403ForbiddenEntryPoint
在认证失败的情况下返回403错误- 如果有一个
EntryPoint
则返回该EntryPoint
- 如果有多个
EntryPoint
则返回DelegatingAuthenticationEntryPoint
,默认使用第一个创建一个
ExceptionTranslationFilter
用于处理所有异常,它将会加入到FilterChain
中获取访问拒绝处理器,获取过程也和
AuthenticationEntryPoint
的获取过程一样
ExceptionHandlingConfigurer.java
AccessDeniedHandler getAccessDeniedHandler(H http) { AccessDeniedHandler deniedHandler = this.accessDeniedHandler;//1 if (deniedHandler == null) {//2 deniedHandler = createDefaultDeniedHandler(http);//3 } return deniedHandler; }
- 获取配置的
AccessDeniedHandler
- 判断是否配置了
AccessDeniedHandler
- 创建默认的
AccessDeniedHandler
我们再看
ExceptionHandlingConfigurer
的createDefaultDeniedHandler
这个方法
ExceptionHandlingConfigurer.java
private AccessDeniedHandler createDefaultDeniedHandler(H http) { if (this.defaultDeniedHandlerMappings.isEmpty()) { return new AccessDeniedHandlerImpl(); } if (this.defaultDeniedHandlerMappings.size() == 1) { return this.defaultDeniedHandlerMappings.values().iterator().next(); } return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings, new AccessDeniedHandlerImpl()); }
如果没有设置默认的
AccessDeniedHandler
则创建一个AccessDeniedHandlerImpl
,该AccessDeniedHandlerImpl
在认证失败的情况下返回以下状态码
HttpStatus.enum
FORBIDDEN(403, Series.CLIENT_ERROR, "Forbidden"),
如果有一个
AccessDeniedHandler
则返回该AccessDeniedHandler
如果有多个
AccessDeniedHandler
则返回RequestMatcherDelegatingAccessDeniedHandler
,默认使用第一个添加访问拒绝处理器到
HttpSecurity
中
分析完ExceptionTranslationFilter
创建的源码,得知它主要是依靠AuthenticationEntryPoint
和AccessDeniedHandler
来处理异常,由此我们自定义异常处理的话就只需自定义EntryPoint
和AccessDeniedHandler
这两个类。
如何自定义这两个类呢?
ExceptionHandlingConfigurer
自定义这两个类的方法:
ExceptionHandlingConfigurer.java
public ExceptionHandlingConfigurer<H> accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
this.accessDeniedHandler = accessDeniedHandler;
return this;
}//1
public ExceptionHandlingConfigurer<H> authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
return this;
}//2
- 设置
accessDeniedHandler
- 设置
authenticationEntryPoint
使用HttpSecurity
中的两个方法进行获取ExceptionHandling
并自定义
HttpSecurity.java
public ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling() throws Exception {
return getOrApply(new ExceptionHandlingConfigurer<>());
}//1
public HttpSecurity exceptionHandling(
Customizer<ExceptionHandlingConfigurer<HttpSecurity>> exceptionHandlingCustomizer) throws Exception {
exceptionHandlingCustomizer.customize(getOrApply(new ExceptionHandlingConfigurer<>()));
return HttpSecurity.this;
}//2
- 获取或创建一个
ExceptionHandlingConfigurer
- 获取或创建一个使用自定义
ExceptionHanding
的ExceptionHandlingConfigurer
我们以第一种为例,自定义配置如下:
MySecurityConfig extends WebSecurityConfigurerAdapter
protected void configure(HttpSecurity httpSecurity) throws Exception {
http.exceptionHandling()//1
.accessDeniedHandler(new MyCustomAccessDeniedHandler())//2
.authenticationEntryPoint(new MyCustomAuthenticationEntryPoint());//3
}
- 获取或创建一个
ExceptionHandlingConfigurer
ExceptionHandlingConfigurer
使用自定义的AccessDeniedHandler()
ExceptionHandlingConfigurer
使用自定义的CustomAuthenticationEntryPoint()
解决了自定义ExceptionHandling
之后我们在来看下面问题:
ExceptionTranlationFilter
是如何处理异常的呢?
我们来看它的doFilter()
方法
ExceptionTranlationFilter.java
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {//1
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);//2
if (securityException == null) {//3
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {//4
rethrow(ex);
}
handleSpringSecurityException(request, response, chain, securityException);//5
}
}
无异常则执行
doFilter
,有IOException
则抛出IOException
异常,其他的异常交给ExceptionTranslationFilter
处理获取
AuthenticationException
异常如果没有
AuthenticationException
异常,则获取AccessDeniedException
异常如果都没有异常,则执行
rethrow(ew)
方法解析rethrow(ew)方法:
ExceptionTranlationFilter.java
private void rethrow(Exception ex) throws ServletException { if (ex instanceof ServletException) { throw (ServletException) ex;//1 } if (ex instanceof RuntimeException) { throw (RuntimeException) ex;//2 } throw new RuntimeException(ex);//3 }
- 如果为ServletException异常,则抛出ServletException异常
- 如果为RuntimeException异常,则抛出RuntimeException异常
- 如果都不是则抛出RuntimeException异常
处理异常
解析 handleSpringSecurityException(request, response, chain, securityException)源码
ExceptionTranslationFilter.java
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { handleAuthenticationException(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception); } }
对两个异常进行分别处理
先解析handleAuthenticationException方法:
ExceptionTranslationFilter.java
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException exception) throws ServletException, IOException { sendStartAuthentication(request, response, chain, exception); } protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { SecurityContext context = SecurityContextHolder.createEmptyContext(); SecurityContextHolder.setContext(context); this.requestCache.saveRequest(request, response);//1 this.authenticationEntryPoint.commence(request, response, reason);//2 }
保存Request信息(以便认证成功后调回原来的页面)
调用AuthenticationEntryPoint中的commenca方法
由于AuthenticationEntryPoint实现类太多,这里只拿
Http403ForbiddenEntryPoint
进行解析:
Http403ForbiddenEntryPoint
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException { logger.debug("Pre-authenticated entry point called. Rejecting access"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); }
总之就是调用AuthenticationEntryPoint对Request和Response进行处理
handleAccessDeniedException
方法的处理流程和AuthenticationEntryPoint一样只是调用
AccessDeniedException
中的handle
方法这里我们只介绍最默认的
AccessDeniedHandlerImpl
的handle
方法public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { if (response.isCommitted()) { logger.trace("Did not write to response since already committed"); return; } if (this.errorPage == null) { logger.debug("Responding with 403 status code"); response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); return; } // Put exception into request scope (perhaps of use to a view) request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); // Set the 403 status code. response.setStatus(HttpStatus.FORBIDDEN.value()); // forward to error page. if (logger.isDebugEnabled()) { logger.debug(LogMessage.format("Forwarding to %s with status code 403", this.errorPage)); } request.getRequestDispatcher(this.errorPage).forward(request, response); }
也是对Response进行处理,并让request跳转到错误页面
到这里异常处理源码解析就告一段落了