exceptionHandling()配置的

这里的配置主要是为了统一处理Spring Security的异常,
Spring Security 中的异常可以分为两大类,一种是认证异常,一种是授权异常。
分别是AuthenticationException 、 AccessDeniedException

简介

1、AuthenticationException

AuthenticationException 异常实现类还是蛮多的,都是都是认证相关的异常,也就是登录失败的异常
image.png

2、AccessDeniedException

授权异常 AccessDeniedException,授权异常的实现类比较少,因为授权失败的可能原因比较少
大部分都是自定义实现授权异常

源码

  1. public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>> extends
  2. AbstractHttpConfigurer<ExceptionHandlingConfigurer<H>, H> {
  3. private AuthenticationEntryPoint authenticationEntryPoint;
  4. private AccessDeniedHandler accessDeniedHandler;
  5. private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<>();
  6. private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
  7. public ExceptionHandlingConfigurer() {
  8. }
  9. @Override
  10. public void configure(H http) {
  11. //身份验证入口点(驱动应用开始进行身份验证),用于启动身份验证方案(默认:Http403ForbiddenEntryPoint)
  12. AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
  13. //处理Filter链中抛出的AccessDeniedException与AuthenticationException类型的异常;
  14. //它提供了Java异常和HTTP响应之间的桥梁
  15. ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
  16. entryPoint, getRequestCache(http));
  17. //默认为AccessDeniedHandlerImpl【响应403】
  18. AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
  19. exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
  20. exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
  21. http.addFilter(exceptionTranslationFilter);
  22. }
  23. AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
  24. AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
  25. if (entryPoint == null) {
  26. entryPoint = createDefaultEntryPoint(http);
  27. }
  28. return entryPoint;
  29. }
  30. private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
  31. if (this.defaultEntryPointMappings.isEmpty()) {
  32. return new Http403ForbiddenEntryPoint();
  33. }
  34. if (this.defaultEntryPointMappings.size() == 1) {
  35. return this.defaultEntryPointMappings.values().iterator().next();
  36. }
  37. DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
  38. this.defaultEntryPointMappings);
  39. entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator()
  40. .next());
  41. return entryPoint;
  42. }
  43. AccessDeniedHandler getAccessDeniedHandler(H http) {
  44. AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
  45. if (deniedHandler == null) {
  46. deniedHandler = createDefaultDeniedHandler(http);
  47. }
  48. return deniedHandler;
  49. }
  50. private AccessDeniedHandler createDefaultDeniedHandler(H http) {
  51. if (this.defaultDeniedHandlerMappings.isEmpty()) {
  52. return new AccessDeniedHandlerImpl();
  53. }
  54. if (this.defaultDeniedHandlerMappings.size() == 1) {
  55. return this.defaultDeniedHandlerMappings.values().iterator().next();
  56. }
  57. return new RequestMatcherDelegatingAccessDeniedHandler(
  58. this.defaultDeniedHandlerMappings,
  59. new AccessDeniedHandlerImpl());
  60. }
  61. }

1、AuthenticationEntryPoint

AuthenticationEntryPoint 说明 验证过滤器
LoginUrlAuthenticationEntryPoint 可以对请求进行重定向
例如将http转换成https请求;
将请求重定向到配置的登录界面(可以服务器端或客户端重定向)
UsernamePasswordAuthenticationFilter
DigestAuthenticationEntryPoint 摘要式身份验证入口点
添加响应头“WWW-Authenticate”
并响应401【未经授权】
DigestAuthenticationFilter
BasicAuthenticationEntryPoint 基础的摘要式身份验证入口点
添加响应头“WWW-Authenticate”
并响应401【未经授权】
BasicAuthenticationFilter

2、ExceptionTranslationFilter

该过滤器的作用是捕获Spring Security的Filter链执行时抛出来的异常,然后根据异常类型来发起验证的流程
默认情况下,这个过滤器已经被自动加载到过滤器链中

当我们使用 Spring Security 的时候,如果需要自定义实现逻辑,都是继承自 WebSecurityConfigurerAdapter 进行扩展,
WebSecurityConfigurerAdapter 中本身就进行了一部分的初始化操作,我们来看下它里边 HttpSecurity 的初始化过程

1、WebSecurityConfigurerAdapter 部份源码

  1. protected final HttpSecurity getHttp() throws Exception {
  2. if (this.http != null) {
  3. return this.http;
  4. } else {
  5. AuthenticationEventPublisher eventPublisher = this.getAuthenticationEventPublisher();
  6. this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
  7. AuthenticationManager authenticationManager = this.authenticationManager();
  8. this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
  9. Map<Class<?>, Object> sharedObjects = this.createSharedObjects();
  10. this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
  11. if (!this.disableDefaults) {
  12. this.applyDefaultConfiguration(this.http);
  13. ClassLoader classLoader = this.context.getClassLoader();
  14. List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
  15. Iterator var6 = defaultHttpConfigurers.iterator();
  16. while(var6.hasNext()) {
  17. AbstractHttpConfigurer configurer = (AbstractHttpConfigurer)var6.next();
  18. this.http.apply(configurer);
  19. }
  20. }
  21. this.configure(this.http);
  22. return this.http;
  23. }
  24. }

在 getHttp 方法的最后,调用了this.configure(http);
说明我们在使用 Spring Security 时,自定义配置类继承自 WebSecurityConfigurerAdapter ,并重写的 configure(HttpSecurity http) 方法就是在这里调用的,换句话说,当我们去配置 HttpSecurity 时,其实它已经完成了一波初始化了

在默认的 HttpSecurity 初始化的过程中,调用了 exceptionHandling 方法,这个方法会将 ExceptionHandlingConfigurer 配置进来,最终调用 ExceptionHandlingConfigurer#configure 方法将 ExceptionTranslationFilter 添加到 Spring Security 过滤器链中

2、ExceptionHandlingConfigurer

public void configure(H http) {
        AuthenticationEntryPoint entryPoint = this.getAuthenticationEntryPoint(http);
        ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, this.getRequestCache(http));
        AccessDeniedHandler deniedHandler = this.getAccessDeniedHandler(http);
        exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
        exceptionTranslationFilter = (ExceptionTranslationFilter)this.postProcess(exceptionTranslationFilter);
        http.addFilter(exceptionTranslationFilter);
    }

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

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

具体的处理逻辑则在 ExceptionTranslationFilter 中

3、ExceptionTranslationFilter

/*
 * Copyright 2004-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.web.access;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

/**
 * 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>
 * To use this filter, it is necessary to specify the following properties:
 * <ul>
 * <li><code>authenticationEntryPoint</code> indicates the handler that should commence
 * the authentication process if an <code>AuthenticationException</code> is detected. Note
 * that this may also switch the current protocol from http to https for an SSL
 * login.</li>
 * <li><tt>requestCache</tt> determines the strategy used to save a request during the
 * authentication process in order that it may be retrieved and reused once the user has
 * authenticated. The default implementation is {@link HttpSessionRequestCache}.</li>
 * </ul>
 *
 * @author Ben Alex
 * @author colin sampaleanu
 */
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {

    private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

    private AuthenticationEntryPoint authenticationEntryPoint;

    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

    private RequestCache requestCache = new HttpSessionRequestCache();

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
        this(authenticationEntryPoint, new HttpSessionRequestCache());
    }

    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
        Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
        Assert.notNull(requestCache, "requestCache cannot be null");
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.requestCache = requestCache;
    }

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationEntryPoint, "authenticationEntryPoint must be specified");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
            RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException) this.throwableAnalyzer
                        .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }
            if (securityException == null) {
                rethrow(ex);
            }
            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception "
                        + "because the response is already committed.", ex);
            }
            handleSpringSecurityException(request, response, chain, securityException);
        }
    }

    private void rethrow(Exception ex) throws ServletException {
        // Rethrow ServletExceptions and RuntimeExceptions as-is
        if (ex instanceof ServletException) {
            throw (ServletException) ex;
        }
        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);
    }

    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return this.authenticationEntryPoint;
    }

    protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
        return this.authenticationTrustResolver;
    }

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

    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);
        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 (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
                        authentication), exception);
            }
            sendStartAuthentication(request, response, chain,
                    new InsufficientAuthenticationException(
                            this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
                                    "Full authentication is required to access this resource")));
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
                        exception);
            }
            this.accessDeniedHandler.handle(request, response, 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.getContext().setAuthentication(null);
        this.requestCache.saveRequest(request, response);
        this.authenticationEntryPoint.commence(request, response, reason);
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
        Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must not be null");
        this.authenticationTrustResolver = authenticationTrustResolver;
    }

    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
        Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
        this.throwableAnalyzer = throwableAnalyzer;
    }

    /**
     * @since 5.5
     */
    @Override
    public void setMessageSource(MessageSource messageSource) {
        Assert.notNull(messageSource, "messageSource cannot be null");
        this.messages = new MessageSourceAccessor(messageSource);
    }

    /**
     * Default implementation of <code>ThrowableAnalyzer</code> which is capable of also
     * unwrapping <code>ServletException</code>s.
     */
    private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {

        /**
         * @see org.springframework.security.web.util.ThrowableAnalyzer#initExtractorMap()
         */
        @Override
        protected void initExtractorMap() {
            super.initExtractorMap();
            registerExtractor(ServletException.class, (throwable) -> {
                ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
                return ((ServletException) throwable).getRootCause();
            });
        }

    }

}

过滤器最核心的当然是 doFilter 方法,我们就从 doFilter 方法看起。
这里的 doFilter 方法中过滤器链继续向下执行,
ExceptionTranslationFilter 处于 Spring Security 过滤器链的倒数第二个,最后一个是 FilterSecurityInterceptor,

FilterSecurityInterceptor 专门处理授权问题,
在处理授权问题时,就会发现用户未登录、未授权等,进而抛出异常,抛出的异常,最终会被 ExceptionTranslationFilter#doFilter 方法捕获。
当捕获到异常之后,接下来通过调用 throwableAnalyzer.getFirstThrowableOfType 方法来判断是认证异常还是授权异常,
判断出异常类型之后,进入到 handleSpringSecurityException 方法进行处理;
如果不是 Spring Security 中的异常类型,则走 ServletException 异常类型的处理逻辑。

进入到 handleSpringSecurityException 方法之后,还是根据异常类型判断,
如果是认证相关的异常,就走 sendStartAuthentication 方法,最终被 authenticationEntryPoint.commence 方法处理;
如果是授权相关的异常,就走 accessDeniedHandler.handle 方法进行处理。

AuthenticationEntryPoint 的默认实现类是 LoginUrlAuthenticationEntryPoint,因此默认的认证异常处理逻辑就是 LoginUrlAuthenticationEntryPoint#commence 方法
image.png

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

自定义处理

前面和大家介绍了 Spring Security 中默认的处理逻辑,实际开发中,我们可以需要做一些调整,
在 exceptionHandling 上进行配置即可

1、自定义异常处理器

首先自定义认证异常处理类和授权异常处理类:

  • 认证异常处理类 ```java @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());
    
    } }

- 授权异常处理类
```java

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

2、配置

在 SecurityConfig 中进行配置

.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)

配置完成后,重启项目,认证异常和授权异常就会走我们自定义的逻辑了