SpringSecurity(二十):异常处理

Posted 刚刚好。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity(二十):异常处理相关的知识,希望对你有一定的参考价值。

Spring Security异常体系

Spirng Security中的异常共有两大类:AuthenticationException(认证异常)和AccessDeniedException(权限异常)

AuthenticationException(认证异常)

AuthenticationException:认证异常的父类,抽象类
BadCredentialsException:登录凭证(密码)异常
InsufficientAuthenticationException:登陆凭证不够充分而抛出的异常
SessionAuthenticationException:会话并发管理时抛出的异常,例如会话总数超出最大限制数
UsernameNotFoundException:用户名不存在异常
PreAuthenticatedCredentialsNotFoundException:身份预认证失败异常
ProviderNotFoundException:未配置AuthenticationProvider异常
AuthenticationServiceException:由于系统问题而无法处理认证请求异常
InternalAuthenticationServiceException:由于系统问题而无法处理认证请求异常,和AuthenticationServiceException不同之处在于如果外部系统出错,不会抛出该异常
AuthenticationCredentialsNotFoundException:SecuityContext 中不存在认证主体时抛出的异常
NonceExpiredException:HTTP摘要认证时随机数过期异常
RememberMeAuthenticationException:RememberMe认证异常
CookieTheftException :RememberMe认证时Cookie被盗窃异常
InvalidCookieException:RememberMe认证时无效的Cookie异常
AccountStatusException:账户状态异常
LockedException:账户被锁定异常
DisabledException:账户被禁用异常
CredentialsExpiredException:登录凭证(密码)过期异常
AccountExpiredException:账户过期异常

AccessDeniedException(权限异常)

AccessDeniedException :权限异常的父类
AuthorizationServiceException: 由于系统问题而无法处理权限时抛出异常
CsrfException:Csrf令牌异常
MissingCsrfTokenException:Csrf令牌缺失异常
InvalidCsrfTokenException:Csrf令牌无效异常

在实际项目中,如果Spring Security提供的这些异常类无法满足需要,开发者也可以根据实际需要自定义异常类

ExceptionTranslationFilter源码分析

在 Spring Security 的过滤器链中,ExceptionTranslationFilter 过滤器专门用来处理异常

该过滤器主要处理AuthenticationException(认证异常)和AccessDeniedException(权限异常),其他异常则会继续抛出,交给上一层容器(Spring)处理

接下来我们来分析ExceptionTranslationFilter 的工作原理

当我们使用 Spring Security 的时候,如果需要自定义实现逻辑,都是继承自 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 方法的最后,调用了 configure(http);,我们在使用 Spring Security 时,自定义配置类继承自 WebSecurityConfigurerAdapter 并重写的 configure(HttpSecurity http) 方法就是在这里调用的,换句话说,当我们去配置 HttpSecurity 时,其实它已经完成了一波初始化了。

在默认的 HttpSecurity 初始化的过程中,调用了 exceptionHandling 方法,这个方法会将 ExceptionHandlingConfigurer 配置进来,最终调用 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);
}

可以看到,这里构造了两个对象传入到 ExceptionTranslationFilter 中:

AuthenticationEntryPoint 这个用来处理认证异常。
AccessDeniedHandler 这个用来处理授权异常。

然后调用postProcess方法把ExceptionTranslationFilter过滤器注册到Spring 容器中,最后调用addFilter方法将其添加在Spring Security过滤器链中

AuthenticationEntryPoint

AuthenticationEntryPoint实例是通过getAuthenticationEntryPoint方法获得的

AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
        AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
        if (entryPoint == null) {
            entryPoint = this.createDefaultEntryPoint(http);
        }

        return entryPoint;
    }



 private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
        if (this.defaultEntryPointMappings.isEmpty()) {
            return new Http403ForbiddenEntryPoint();
        } else if (this.defaultEntryPointMappings.size() == 1) {
            return (AuthenticationEntryPoint)this.defaultEntryPointMappings.values().iterator().next();
        } else {
            DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(this.defaultEntryPointMappings);
            entryPoint.setDefaultEntryPoint((AuthenticationEntryPoint)this.defaultEntryPointMappings.values().iterator().next());
            return entryPoint;
        }
    }


默认情况下 系统的authenticationEntryPoint为null,所以最终还是通过createDefaultEntryPoint方法来获取一个AuthenticationEntryPoint实例。在createDefaultEntryPoint中有一个defaultEntryPointMappings,它是一个LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> 类型,他的key是一个请求匹配器,而value是一个认证失败处理器,即一个请求匹配器对应一个认证失败处理器。换句话说,针对不同的请求,可以给出不同的认证失败处理器,如果defaultEntryPointMappings变量为空,则返回一个Http403ForbiddenEntryPoint类型的处理器;如果defaultEntryPointMappings变量中只有一项,则将这一项取出来返回即可;如果defaultEntryPointMappings有多项,则使用DelegatingAuthenticationEntryPoint代理类,遍历defaultEntryPointMappings中每一项,查看当前请求是否满足其RequestMatcher,如果满足,则使用对应的认证失败处理器来处理

当我们新建一个Spring Security项目,不做任何配置时,在WebSecurityConfigurerAdapter的configure(HttpSecurity)方法中默认会配置表单登录和HTTP基本认证,表单登录和HTTP基本认证在配置的过程中,会分别向defaultEntryPointMappings变量中添加认证失败处理器,所以defaultEntryPointMappings中默认有两个认证失败处理器

AuthenticationEntryPoint 的默认实现类是 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

AccessDeniedHandler实例通过getAccessDeniedHandler方法获取

AccessDeniedHandler getAccessDeniedHandler(H http) {
        AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
        if (deniedHandler == null) {
            deniedHandler = this.createDefaultDeniedHandler(http);
        }

        return deniedHandler;
    }





private AccessDeniedHandler createDefaultDeniedHandler(H http) {
        if (this.defaultDeniedHandlerMappings.isEmpty()) {
            return new AccessDeniedHandlerImpl();
        } else {
            return (AccessDeniedHandler)(this.defaultDeniedHandlerMappings.size() == 1 ? (AccessDeniedHandler)this.defaultDeniedHandlerMappings.values().iterator().next() : new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings, new AccessDeniedHandlerImpl()));
        }
    }

可以看到 AccessDeniedHandler实例获取流程和AuthenticationEntryPoint 的获取流程基本一模一样,也有一个defaultDeniedHandlerMappings,也可以为不同的路径配置不同的鉴权失败处理器

不同的是,默认情况下,defaultDeniedHandlerMappings是空的,所以最终获取的实例是AccessDeniedHandlerImpl 在AccessDeniedHandlerImpl 中的handle方法中处理鉴权失败的情况,如果存在错误页面就直接跳转到错误页面,并设置响应码为403;如果没有错误页面,则直接给出错误响应

AccessDeniedHandler 的默认实现类则是 AccessDeniedHandlerImpl,所以授权异常默认是在 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());
        }
    }
}

可以看到,这里就是服务端跳转返回 403。

ExceptionTranslationFilter

AccessDeniedHandler和AuthenticationEntryPoint 都有了,接下来是ExceptionTranslationFilter的处理逻辑。

默认情况下ExceptionTranslationFilter过滤器在整个Spring Security过滤器链中排名倒数第二,倒数第一是FilterSecurityInterceptor。在FilterSecurityInterceptor中将会对用户的身份进行校验,如果用户身份不合法,就会抛出异常,抛出的异常刚好就在ExceptionTranslationFilter中处理了

下面我们来看下doFilter方法

 private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (IOException var7) {
            throw var7;
        } catch (Exception var8) {
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
            RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (securityException == null) {
                this.rethrow(var8);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
            }

            this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
        }

    }

可以看到,在该过滤器中直接执行了chain.doFilter方法,让当前请求继续执行剩下的过滤器(FilterSecurityInterceptor),然后用一个try catch代码块将chain.doFilter包裹起来,如果有异常就直接在这里捕获到了

throwableAnalyzer对象是一个异常分析器,由于异常在抛出的过程中可能被“层层转包”,我们需要还原最初的异常,通过determineCauseChain方法可以获得整个异常链(转成一个数组)

所以在catch块中捕获到异常后,首先获得异常链,然后调用getFirstThrowableOfType方法判断异常链是否有认证失败类型的异常AuthenticationException,如果不存在再去查找是否有AccessDeniedException,如果存在这两种类型的异常,则调用handleSpringSecurityException方法进行异常处理,否则把异常交给上层容器处理

我们来看下handleSpringSecurityException方法

 private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            this.handleAuthenticationException(request, response, chain, (AuthenticationException)exception);
        } else if (exception instanceof AccessDeniedException) {
            this.handleAccessDeniedException(request, response, chain, (AccessDeniedException)exception);
        }

    }



  private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
        this.logger.trace("Sending to authentication entry point since authentication failed", exception);
        this.sendStartAuthentication(request, response, chain, exception);
    }



  private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
        if (!isAnonymous && !this.authenticationTrustResolver.isRememberMe(authentication)) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Sending %s to access denied handler since access is denied", authentication), exception);
            }

            this.accessDeniedHandler.handle(request, response, exception);
        } else {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception);
            }

            this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
        }

    }

可以看到,如果是AuthenticationException,则调用sendStartAuthentication方法

  protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
        SecurityContextHolder.getContext().setAuthentication((Authentication)null);
        this.requestCache.saveRequest(request, response);
        this.authenticationEntryPoint.commence(request, response, reason);
    }

这里做了三件事:1.清除SecurityContextHolder中保存的认证主体 2.保存当前请求 3.调用authenticationEntryPoint.commence完成认证失败处理

如果是AccessDeniedException,则取出当前认证主体,如果是匿名用户或者认证是通过rememberMe完成的,那么认为是AuthenticationException(认证异常),重新创建一个InsufficientAuthenticationException异常对象,交由sendStartAuthentication方法处理。如果不是,由accessDeniedHandler的handle方法处理

自定义异常配置

经过上面的分析,我们可以看到异常处理在AuthenticationEntryPoint的commence方法 和 AccessDeniedHandler的handle方法 中,那么我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler的实现类,并重写commence方法 和 handle方法

@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());
    }
}

然后设置到spring security中

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                ...
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                ...
                ...
    }
}

以上是关于SpringSecurity(二十):异常处理的主要内容,如果未能解决你的问题,请参考以下文章

python学习笔记(二十):异常处理

SpringBoot集成SpringSecurity - 异常处理(三)

SpringBoot入门二十一,全局异常处理

二十异常捕获及处理详解

《C#零基础入门之百识百例》(二十)异常处理 -- 除数为0

Python基础(二十五):异常处理基础知识