异常处理主要是由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 ExceptionHandlingConfigureraccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { this.accessDeniedHandler = accessDeniedHandler; return this; }//1 public ExceptionHandlingConfigurer authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) { this.authenticationEntryPoint = authenticationEntryPoint; return this; }//2
- 设置accessDeniedHandler
- 设置authenticationEntryPoint
使用HttpSecurity中的两个方法进行获取ExceptionHandling并自定义
HttpSecurity.java
public ExceptionHandlingConfigurerexceptionHandling() throws Exception { return getOrApply(new ExceptionHandlingConfigurer<>()); }//1 public HttpSecurity exceptionHandling( Customizer > 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跳转到错误页面
到这里异常处理源码解析就告一段落了



