ExceptionTranslationFilter
介绍
Spring Security授权认证服务的异常处理不能像常规Spring MVC或者Spring Boot那样进行统一异常处理,而是在过滤器上进行了层层拦截,代码阅读起来也有些费劲。而该Filter是用于处理Spring Security部分异常的,开发者可以自定义accessDeniedHandler和authenticationEntryPoint进行配置,根据自己的需求进行异常处理。
代码分析
步骤1
上篇文章已经提及过Spring Security认证服务配置分ResourceServerConfigurerAdapter(资源认证服务配置)和AuthorizationServerSecurityConfigurer(授权认证服务配置),对于授权认证服务,作者还没找到办法自定义注入accessDeniedHandler和authenticationEntryPoint,我们可以看一下WebSecurityConfigurerAdapter的部分代码:
protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
}
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
.postProcess(new DefaultAuthenticationEventPublisher());
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManager);
authenticationBuilder.authenticationEventPublisher(eventPublisher);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
//添加一个ExceptionHandlingConfigurer,configurer是new出来的,未注入accessDeniedHandler和authenticationEntryPoint
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
configure(http);
return http;
}
configure(http)这行代码将执行AuthorizationServerSecurityConfiguration#configure()方法
protected void configure(HttpSecurity http) throws Exception {
//这里的configurer是new出来的
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
//这里也为改变ExceptionHandlingConfigurer的accessDeniedHandler和authenticationEntryPoint
//只是调用的自定义的AuthorizationServerConfigurerAdapter
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// @formatter:off
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
// @formatter:on
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
http.apply(configurer)这行代码会执行到我们自定义的AuthorizationServerConfigurerAdapter#configure(AuthorizationServerSecurityConfigurer security)方法,如下:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess(TokenAccess.PERMIT_ALL)
.checkTokenAccess(TokenAccess.PERMIT_ALL)
.allowFormAuthenticationForClients()
//这里的配置之会改变AuthorizationServerSecurityConfiguration中对应的对象
.accessDeniedHandler(new CarpAccessDeniedHandler(objectMapper))
.authenticationEntryPoint(new AuthorizeAuthExceptionEntryPoint(objectMapper))
//使用ObjectPostProcessor也不行
.addObjectPostProcessor(new ObjectPostProcessor<ExceptionHandlingConfigurer>() {
@Override
public <O extends ExceptionHandlingConfigurer> O postProcess(O object) {
object.accessDeniedHandler(new CarpAccessDeniedHandler(objectMapper));
return (O) object.authenticationEntryPoint(new AuthorizeAuthExceptionEntryPoint(objectMapper));
}
});
}
笔者在授权认证服务上注入自定义accessDeniedHandler和authenticationEntryPoint失败,但是比如我们的业务系统引入的是资源认证配置,是可以通过如下的方法注入的,配置如下:
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new AuthorizeAuthExceptionEntryPoint(objectMapper));
http.exceptionHandling().accessDeniedHandler(new CarpAccessDeniedHandler(objectMapper));
}
步骤2
ExceptionTranslationFilter过滤执行时对异常进行捕获,并从异常堆栈中提取了SpringSecurityException,针对AuthenticationException以及AccessDeniedException过滤器做了特殊化处理。有一点需要注意的是,Spring Security在很多地方都做异常的捕获,并对异常做了转换,意味着你的业务代码抛出的异常很可能被Spring Security转换,doFilter()核心代码如下:
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) {
// Try to extract a SpringSecurityException from the stacktrace
//从异常堆栈中提取SpringSecurityException
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
//找到第一个AuthenticationException异常
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
//找到第一个AccessDeniedException异常
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 {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
//清空SecurityContextHolder的Authentication
SecurityContextHolder.getContext().setAuthentication(null);
//缓存请求
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
//类似切点,处理请求以及响应
authenticationEntryPoint.commence(request, response, reason);
}