今天來和小夥伴們聊一聊 Spring Security 中的異常處理機制。java
在 Spring Security 的過濾器鏈中,ExceptionTranslationFilter 過濾器專門用來處理異常,在 ExceptionTranslationFilter 中,咱們能夠看到,異常被分爲了兩大類:認證異常和受權異常,兩種異常分別由不一樣的回調函數來處理,今天鬆哥就來和你們分享一下這裏的條條框框。git
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,受權異常的實現類比較少,由於受權失敗的可能緣由比較少。後端
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 中:
具體的處理邏輯則在 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 的源碼比較長,我這裏列出來核心的部分和你們分析:
throwableAnalyzer.getFirstThrowableOfType
方法來判斷是認證異常仍是受權異常,判斷出異常類型以後,進入到 handleSpringSecurityException 方法進行處理;若是不是 Spring Security 中的異常類型,則走 ServletException 異常類型的處理邏輯。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。
前面和你們介紹了 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() ... ... } }
配置完成後,重啓項目,認證異常和受權異常就會走咱們自定義的邏輯了。
好啦,今天主要和小夥伴們分享了 Spring Security 中的異常處理機制,感興趣的小夥伴能夠試一試哦~
文中代碼下載地址:https://github.com/lenve/spring-security-samples
公衆號【江南一點雨】後臺回覆 springsecurity,獲取Spring Security系列 40+ 篇完整文章~