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.