史上最简单的Spring Security教程(三十八):ExceptionTranslationFilter详解

 

这是 Spring Security 框架 FilterChain 中倒数第二个 Filter,承载着承上启下的作用。还记得 FilterSecurityInterceptor 吗?史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解

如当用户未登录时抛出的 AccessDeniedException 异常、或者其它 AuthenticationException 异常。此时,系统会跳转到固定的页面,如 403 页面、或者执行其它操作。

这是怎么实现的呢?

其实,这就是 ExceptionTranslationFilter 实现的。

先来看看类注释。

/**
 * Handles any <code>AccessDeniedException</code> and <code>AuthenticationException</code>
 * thrown within the filter chain.
 * <p>
 * This filter is necessary because it provides the bridge between Java exceptions and
 * HTTP responses. It is solely concerned with maintaining the user interface. This filter
 * does not do any actual security enforcement.
 * <p>
 * If an {@link AuthenticationException} is detected, the filter will launch the
 * <code>authenticationEntryPoint</code>. This allows common handling of authentication
 * failures originating from any subclass of
 * {@link org.springframework.security.access.intercept.AbstractSecurityInterceptor}.
 * <p>
 * If an {@link AccessDeniedException} is detected, the filter will determine whether or
 * not the user is an anonymous user. If they are an anonymous user, the
 * <code>authenticationEntryPoint</code> will be launched. If they are not an anonymous
 * user, the filter will delegate to the
 * {@link org.springframework.security.web.access.AccessDeniedHandler}. By default the
 * filter will use {@link org.springframework.security.web.access.AccessDeniedHandlerImpl}.
 * <p>
 */

此 Filter 用于处理任何 AccessDeniedException 和 AuthenticationException 异常, 承担着 Java 异常 和 HTTP 状态码之间的桥梁作用,并没有任何其它的实质性作用。

还记得回调吗?

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);
​
        logger.debug("Chain processed normally");
    }

后续 Filter 执行过程中发生异常后,会被此处捕获,这便是 Filter 回调的秘密

异常是如何处理的呢?

/**
 * If an {@link AuthenticationException} is detected, the filter will launch the
 * <code>authenticationEntryPoint</code>. This allows common handling of authentication
 * failures originating from any subclass of
 * {@link org.springframework.security.access.intercept.AbstractSecurityInterceptor}.
 */

意思即为,如果发生 AuthenticationException 异常,此 Filter 会使用 AuthenticationEntryPoint 来通用处理任何 AbstractSecurityInterceptor 子类出现的认证失败逻辑。

首先,就是捕获后续 Filter 发生的异常,然后,解析异常类型:AuthenticationException、AccessDeniedException。

catch (Exception ex) {
    // Try to extract a SpringSecurityException from the stacktrace
    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 {
        // Rethrow ServletExceptions and RuntimeExceptions as-is
        if (ex instanceof ServletException) {
            throw (ServletException) ex;
        }
        else if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
        }
​
        // Wrap other Exceptions. This shouldn't actually happen
        // as we've already covered all the possibilities for doFilter
        throw new RuntimeException(ex);
    }
}

然后调用 handleSpringSecurityException 方法处理异常。

如果是 AuthenticationException 异常,则调用 AuthenticationEntryPoint 来处理。

private void handleSpringSecurityException(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, RuntimeException exception)
      throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
        logger.debug(
            "Authentication exception occurred; redirecting to authentication entry point",
            exception);
​
        sendStartAuthentication(request, response, chain,
                                (AuthenticationException) exception);
    }
    
  ......
}

如果是 AccessDeniedException 异常,先判断是否是未登录状态或者是记住我状态,如果是这两种状态,则调用 AuthenticationEntryPoint 来通用处理。

private void handleSpringSecurityException(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, RuntimeException exception)
      throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
        ......
    }
    else if (exception instanceof AccessDeniedException) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
            logger.debug(
                "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
                exception);
​
            sendStartAuthentication(
                request,
                response,
                chain,
                new InsufficientAuthenticationException(
                    messages.getMessage(
                        "ExceptionTranslationFilter.insufficientAuthentication",
                        "Full authentication is required to access this resource")));
        }
    ......  
    }
}

这两种情况都调用了 sendStartAuthentication 方法来处理,而其逻辑便是先清空失效的 Authentication,然后再调用 AuthenticationEntryPoint 来通用处理异常。

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.getContext().setAuthentication(null);
    requestCache.saveRequest(request, response);
    logger.debug("Calling Authentication entry point.");
    authenticationEntryPoint.commence(request, response, reason);
}

如果发生未 AccessDeniedException 异常时,既不是登录状态,也不是是记住我状态,则会调用 AccessDeniedHandler 来处理异常。

private void handleSpringSecurityException(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, RuntimeException exception)
      throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
      ......
    }
    else if (exception instanceof AccessDeniedException) {
      ......
      }
      else {
        logger.debug(
            "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
            exception);
​
        accessDeniedHandler.handle(request, response,
            (AccessDeniedException) exception);
      }
    }
  }

AccessDeniedHandler 默认使用 AccessDeniedHandlerImpl 实现,会根据情况跳转到错误页面,或者发送对应的HTTP状态码。这就是文章开头所说的介于Java异常和HTTP状态码之间的桥梁作用

public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException,
      ServletException {
    if (!response.isCommitted()) {
      if (errorPage != null) {
        // 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.
        RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
        dispatcher.forward(request, response);
      }
      else {
        response.sendError(HttpStatus.FORBIDDEN.value(),
          HttpStatus.FORBIDDEN.getReasonPhrase());
      }
    }
  }

至于 AuthenticationEntryPoint  是何方神圣、而 ExceptionTranslationFilter 又是如何自动化配置的,我们后续再说。

其它详细源码,请参考文末源码链接,可自行下载后阅读。

我是银河架构师,十年饮冰,难凉热血,愿历尽千帆,归来仍是少年! 

如果文章对您有帮助,请举起您的小手,轻轻【三连】,这将是笔者持续创作的动力源泉。当然,如果文章有错误,或者您有任何的意见或建议,请留言。感谢您的阅读!

 

源码

 

github

https://github.com/liuminglei/SpringSecurityLearning/tree/master/38

gitee

https://gitee.com/xbd521/SpringSecurityLearning/tree/master/38

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值