Handle Custom Exceptions in Spring Security

2020-05-23 02:42发布

问题:

We are creating a RESTful API with spring MVC + spring security + hibernate. The API can produce both JSON and HTML. Doing a good error handling for spring security is giving me a headache:

Authentication can happen in various ways: BasicAuth, via different parameters in a POST request and also via web log-in. For each authentication mechanism, there is a filter declared in the <http> namespace element of the spring security xml config.

We handle all our spring exceptions in a custom HandlerExceptionResolver. This works fine for all exceptions thrown in our controllers, but I don't know how to handle custom Exceptions thrown in the custom spring security filters. Since the spring security filter comes before any of our controllers are invoked we do not see exceptions that we throw in our custom spring security filters.

I found this question here on stackoverflow: Use custom exceptions in Spring Security. However I don't understand where they handle the exceptions that are thrown there. We tried this approach but our custom HandlerExceptionResolver is not called. Instead the user is presented with an ugly stacktrace rendered by tomcat.

Why do we need this? Users can be activated and deactivated. If they are deactivated and try to perform certain actions we would like to return JSON with a custom error message. This should be different than what is displayed when spring security throws a AccessDeniedException. The AccessDeniedException somehow makes it to our HandlerExceptionResolver, but I could not follow how exactly.

Possible solution We thought about using an ExceptionTranslationFilter, however this is not called when we throw our custom exceptions (set a breakpoint in the catch statement of the doFilter() method). In my understanding this catch block should be called and an authentication entry point should be used.

Another possibility: We could do something similar to the ExceptionTranslationFilter in the spring security filter chain and do something similar to what its AccessDeniedHandler does:

RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);

We could add some parameters (error code, reason etc.) to the request and have it point to a controller which would take care of the rendering in JSON or HTML.

Here is a short excerpt of our configuration:

Spring Security:

<http create-session="stateless" use-expressions="true" >
    <!-- Try getting the authorization object from the request parameters. -->
    <security:custom-filter ref="filter1" after="SECURITY_CONTEXT_FILTER"/>
    <security:custom-filter ref="filter2" before="LOGOUT_FILTER"/>
    <!-- Intercept certain URLS differently -->

    <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />
    <!-- Some more stuff here -->
    <intercept-url pattern="/**" access="denyAll" />  
    <http-basic />
</http>

AppConfig of the HandlerExceptionResolver

@Bean
public HandlerExceptionResolver handlerExceptionResolver(){
    logger.info("creating handler exception resolver");
    return new AllExceptionHandler();
}

Our custom HandlerExceptionResolver

public class AllExceptionHandler implements HandlerExceptionResolver {

    private static final Logger logger = LoggerFactory
        .getLogger(AppConfig.class);

    @Override
    public ModelAndView resolveException(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex) {
    // This is just a snipped of the real method code
    return new ModelAndView("errorPage");
}

The relevant part of one of our filters:

try {
    Authentication authResult = authenticationManger.authenticate(authRequest);
    SecurityContextHolder.getContext().setAuthentication(authResult);
}

catch(AuthenticationException failed) {
    SecurityContextHolder.clearContext();
    throw failed; 
}

Web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>xxx.xxx.xxx.config</param-value>
</context-param>
<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>LIVE</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value></param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <!-- Add multipart support for files up to 10 MB -->
    <multipart-config>
        <max-file-size>10000000</max-file-size>
    </multipart-config>
</servlet>
<servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
    <filter-name>openEntityManagerInViewFilter</filter-name>
    <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>openEntityManagerInViewFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<!-- Map filters -->
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<error-page>
    <error-code>404</error-code>
    <location>/handle/404</location>
</error-page>
</web-app>

Does anyone have any pointers on how we could solve this? I looked through many articles on google, most of them describe how to handle the AccessDeniedException thrown by spring security when no filter is able to authenticate the request.

We're using Spring Security 3.1.0 and spring web mvc 3.1.0.

回答1:

It's important to remember that the order of the filters in Spring Security matters.

From Spring Security 3 book:

The ExceptionTranslationFilter will be able to handle and react to only those exceptions that are thrown below it in the filter chain execution stack. Users often get confused, especially when adding custom filters in the incorrect order, as to why the expected behavior differs from their application's actual exception handling—in many of these cases, the order of the filters is to blame!

If your filters are about authorization it is a good practice to put them a the end of the chain as this approach is used by default authorization filters. That way you don't have to reinvent the wheel.

Standard filters: Table in documentation

After you properly configured your filter chain, you can configure error page, or even custom handler. More information available in documentation.



回答2:

I see that ExceptionTranslationFilter only handles two exceptions AuthenticationException and AccessDeniedException with custom handlers for these two exceptions, what about any other type of exception or even run time exceptions?

How would you handle/intercept just about any exception in Spring filter stack? Isn't there any Spring standard way to catch and get request (besides writing a custom filter on top of everything), response without writing another filter on top of everything?

<security:http auto-config="false" use-expressions="true"
disable-url-rewriting="true" entry-point-ref="authenticationEntryPoint"
pattern="/**">

<security:custom-filter before="FIRST" ref="stackExceptionFilter" />

<security:custom-filter before="..." ref="authenticationFilter" />
<security:logout />
</security:http> 

Well, I ended up adding another filter right on top (or configure the filter for /* in web.xml) that simply had try catch block and delegated any uncaught exception to a custom exception handler Spring component calling an ExceptionController method (each method returning different response type in different ways) in a fail safe way also returning custom exception messages based on exception type (our requirement). The only down part was to add some logic so you won't keep looping. The Spring custom ExceptionHandlerExceptionResolver and @ExceptionHandler in controllers do not handle filter exceptions and have limitation on how you want to return an exception message as (XML/JSON, redirect, forward,....). This assumes you have good Application Exception hierarchy that catches exceptions and throws them with sensible reference information as filters don't have anything.

Same for error codes, define static pages in web.xml but do catch them by mapping a filter to the ERROR dispatcher and preparing model for the pages displaying the error code.