一文搞定 Spring Security 異常處理機制!

今天來和小夥伴們聊一聊 Spring Security 中的異常處理機制。java

在 Spring Security 的過濾器鏈中,ExceptionTranslationFilter 過濾器專門用來處理異常,在 ExceptionTranslationFilter 中,咱們能夠看到,異常被分爲了兩大類:認證異常和受權異常,兩種異常分別由不一樣的回調函數來處理,今天鬆哥就來和你們分享一下這裏的條條框框。git

1.異常分類

Spring Security 中的異常能夠分爲兩大類,一種是認證異常,一種是受權異常。github

認證異常就是 AuthenticationException,它有衆多的實現類:spring

能夠看到,這裏的異常實現類仍是蠻多的,都是都是認證相關的異常,也就是登陸失敗的異常。這些異常,有的鬆哥在以前的文章中都和你們介紹過了,例以下面這段代碼(節選自:Spring Security 作先後端分離,咱就別作頁面跳轉了!通通 JSON 交互):json

resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanceof LockedException) {
    respBean.setMsg("帳戶被鎖定,請聯繫管理員!");
} else if (e instanceof CredentialsExpiredException) {
    respBean.setMsg("密碼過時,請聯繫管理員!");
} else if (e instanceof AccountExpiredException) {
    respBean.setMsg("帳戶過時,請聯繫管理員!");
} else if (e instanceof DisabledException) {
    respBean.setMsg("帳戶被禁用,請聯繫管理員!");
} else if (e instanceof BadCredentialsException) {
    respBean.setMsg("用戶名或者密碼輸入錯誤,請從新輸入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();

另外一類就是受權異常 AccessDeniedException,受權異常的實現類比較少,由於受權失敗的可能緣由比較少。後端

2.ExceptionTranslationFilter

ExceptionTranslationFilter 是 Spring Security 中專門負責處理異常的過濾器,默認狀況下,這個過濾器已經被自動加載到過濾器鏈中。session

有的小夥伴可能不清楚是怎麼被加載的,我這裏和你們稍微說一下。app

當咱們使用 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 時,其實它已經完成了一波初始化了。ide

在默認的 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 這個用來處理受權異常。

具體的處理邏輯則在 ExceptionTranslationFilter 中,咱們來看一下:

public class ExceptionTranslationFilter extends GenericFilterBean {
    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
            RequestCache requestCache) {
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.requestCache = requestCache;
    }
    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) {
            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 {
                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 {
        SecurityContextHolder.getContext().setAuthentication(null);
        requestCache.saveRequest(request, response);
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }
}

ExceptionTranslationFilter 的源碼比較長,我這裏列出來核心的部分和你們分析:

  1. 過濾器最核心的固然是 doFilter 方法,咱們就從 doFilter 方法看起。這裏的 doFilter 方法中過濾器鏈繼續向下執行,ExceptionTranslationFilter 處於 Spring Security 過濾器鏈的倒數第二個,最後一個是 FilterSecurityInterceptor,FilterSecurityInterceptor 專門處理受權問題,在處理受權問題時,就會發現用戶未登陸、未受權等,進而拋出異常,拋出的異常,最終會被 ExceptionTranslationFilter#doFilter 方法捕獲。
  2. 當捕獲到異常以後,接下來經過調用 throwableAnalyzer.getFirstThrowableOfType 方法來判斷是認證異常仍是受權異常,判斷出異常類型以後,進入到 handleSpringSecurityException 方法進行處理;若是不是 Spring Security 中的異常類型,則走 ServletException 異常類型的處理邏輯。
  3. 進入到 handleSpringSecurityException 方法以後,仍是根據異常類型判斷,若是是認證相關的異常,就走 sendStartAuthentication 方法,最終被 authenticationEntryPoint.commence 方法處理;若是是受權相關的異常,就走 accessDeniedHandler.handle 方法進行處理。

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 的默認實現類則是 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。

3.自定義處理

前面和你們介紹了 Spring Security 中默認的處理邏輯,實際開發中,咱們能夠須要作一些調整,很簡單,在 exceptionHandling 上進行配置便可。

首先自定義認證異常處理類和受權異常處理類:

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

而後在 SecurityConfig 中進行配置,以下:

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

配置完成後,重啓項目,認證異常和受權異常就會走咱們自定義的邏輯了。

4.小結

好啦,今天主要和小夥伴們分享了 Spring Security 中的異常處理機制,感興趣的小夥伴能夠試一試哦~

文中代碼下載地址:https://github.com/lenve/spring-security-samples

公衆號【江南一點雨】後臺回覆 springsecurity,獲取Spring Security系列 40+ 篇完整文章~

相關文章
相關標籤/搜索