Today, let's talk about the exception handling mechanism in Spring Security.
In Spring Security's filter chain, the ExceptionTranslationFilter filter is specially used to handle exceptions. In the ExceptionTranslationFilter, we can see that exceptions are divided into two categories: authentication exceptions and authorization exceptions. The two exceptions are handled by different callback functions. Today, SongGe will share the rules and regulations here with you.
1. Anomaly classification
Exceptions in Spring Security can be divided into two categories, one is authentication exception, the other is authorization exception.
Authentication exception is AuthenticationException, which has many implementation classes
As you can see, there are many exception implementation classes here, all of which are authentication related exceptions, that is, login failure exceptions. Some of these exceptions have been introduced by SongGe in previous articles, such as the following code (excerpt from: Spring Security does front-end and back-end separation, so we don't do page Jump! All JSON interaction):
resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); RespBean respBean = RespBean.error(e.getMessage()); if (e instanceof LockedException) { respBean.setMsg("Account locked, please contact administrator!"); } else if (e instanceof CredentialsExpiredException) { respBean.setMsg("Password expired, please contact the administrator!"); } else if (e instanceof AccountExpiredException) { respBean.setMsg("Account expired, please contact administrator!"); } else if (e instanceof DisabledException) { respBean.setMsg("Account is disabled, please contact administrator!"); } else if (e instanceof BadCredentialsException) { respBean.setMsg("User name or password input error, please re-enter!"); } out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close();
The other is the authorization exception AccessDeniedException. There are fewer implementation classes for authorization exceptions, because there are fewer possible reasons for authorization failure.
2.ExceptionTranslationFilter
ExceptionTranslationFilter is a special exception handling filter in Spring Security. By default, this filter has been automatically loaded into the filter chain.
Some of the partners may not know how to load them. I'll tell you a little bit.
When we use Spring Security, if we need to customize the implementation logic, they are all extended from websecurity configureradapter. Websecurity configureradapter itself carries out part of the initialization operation. Let's take a look at the initialization process of HttpSecurity in it
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; }
As you can see, at the end of the getHttp method, configure(http) is called; when we use Spring Security, the custom configuration class inherits from websecurity configureradapter and rewrites the configure(HttpSecurity http) method is called here. In other words, when we configure HttpSecurity, it has already completed a wave of initialization.
In the default HttpSecurity initialization process, the exceptionHandling method is called. This method will configure the ExceptionHandlingConfigurer, and finally call the ExceptionHandlingConfigurer ා configure method to add the ExceptionTranslationFilter to the Spring Security filter chain.
Let's take a look at the source code of the exceptionhandlingconfigurer ා configure method:
@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); }
As you can see, two objects are constructed and passed into the ExceptionTranslationFilter:
- Authentication entrypoint is used to handle authentication exceptions.
- AccessDeniedHandler is used to handle authorization exceptions.
The specific processing logic is in the 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); } }
The source code of ExceptionTranslationFilter is relatively long. Here I will list the core parts and analyze it
- The core of the filter is of course the doFilter method. Let's start with the doFilter method. In the doFilter method, the filter chain continues to execute downward. ExceptionTranslationFilter is in the penultimate of Spring Security filter chain, and the last one is FilterSecurityInterceptor and FilterSecurityInterceptor When dealing with authorization problems, it will be found that the user is not logged in or not authorized, and then an exception will be thrown. The thrown exception will eventually be caught by the ExceptionTranslationFilter ා doFilter method.
- When the exception is caught, the next step is to call the throwableAnalyzer.getFirstThrowableOfType Method to determine whether it is an authentication exception or an authorization exception. After judging the exception type, enter the handleSpringSecurityException method for processing; if it is not the exception type in Spring Security, follow the processing logic of ServletException exception type.
- After entering the handleSpringSecurityException method, it is still judged according to the exception type. If it is an authentication related exception, the sendStartAuthentication method is used, and finally the auth method is used enticationEntryPoint.commence Method; if it is an authorization related exception, go accessDeniedHandler.handle Methods.
The default implementation class of AuthenticationEntryPoint is LoginUrlAuthenticationEntryPoint. Therefore, the default authentication exception handling logic is the LoginUrlAuthenticationEntryPoint ා commit method, as follows:
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); }
As you can see, it is redirection to the login page (that is, when we visit a resource that needs to be logged in to access without logging in, it will be automatically redirected to the login page).
The default implementation class of AccessDeniedHandler is AccessDeniedHandlerImpl, so the authorization exception is handled in the AccessDeniedHandlerImpl ා handle method by default
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()); } } }
As you can see, here is the server jump back 403.
3. User defined processing
We have introduced the default processing logic in Spring Security. In actual development, we can make some adjustments. It is very simple. You can configure it on exceptionHandling.
First, define the authentication exception handling class and authorization exception handling class
@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()); } }
Then configure it in SecurityConfig, as follows:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ... ... .and() .exceptionHandling() .authenticationEntryPoint(myAuthenticationEntryPoint) .accessDeniedHandler(myAccessDeniedHandler) .and() ... ... } }
After the configuration is completed, restart the project, and the authentication exception and authorization exception will follow our custom logic.
4. Summary
Well, today I mainly shared with my friends the exception handling mechanism in Spring Security. If you are interested, you can have a try
Code download address: https://github.com/lenve/spring-security-samples
The official account [Jiangnan rain] backstage reply springsecurity, get the complete articles of Spring Security series 40+ to </abstracthttpconfigurer></class< > >