Handle spring security authentication exceptions with @ExceptionHandler
Ok, I tried as suggested writing the json myself from the AuthenticationEntryPoint and it works.
Just for testing I changed the AutenticationEntryPoint by removing response.sendError
@Component("restAuthenticationEntryPoint")public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getOutputStream().println("{ \"error\": \"" + authenticationException.getMessage() + "\" }"); }}
In this way you can send custom json data along with the 401 unauthorized even if you are using Spring Security AuthenticationEntryPoint.
Obviously you would not build the json as I did for testing purposes but you would serialize some class instance.
In Spring Boot, you should add it to http.authenticationEntryPoint() part of SecurityConfiguration file.
This is a very interesting problem that Spring Security and Spring Web framework is not quite consistent in the way they handle the response. I believe it has to natively support error message handling with MessageConverter
in a handy way.
I tried to find an elegant way to inject MessageConverter
into Spring Security so that they could catch the exception and return them in a right format according to content negotiation. Still, my solution below is not elegant but at least make use of Spring code.
I assume you know how to include Jackson and JAXB library, otherwise there is no point to proceed. There are 3 Steps in total.
Step 1 - Create a standalone class, storing MessageConverters
This class plays no magic. It simply stores the message converters and a processor RequestResponseBodyMethodProcessor
. The magic is inside that processor which will do all the job including content negotiation and converting the response body accordingly.
public class MessageProcessor { // Any name you like // List of HttpMessageConverter private List<HttpMessageConverter<?>> messageConverters; // under org.springframework.web.servlet.mvc.method.annotation private RequestResponseBodyMethodProcessor processor; /** * Below class name are copied from the framework. * (And yes, they are hard-coded, too) */ private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", MessageProcessor.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", MessageProcessor.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", MessageProcessor.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", MessageProcessor.class.getClassLoader()); public MessageProcessor() { this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); this.messageConverters.add(new ResourceHttpMessageConverter()); this.messageConverters.add(new SourceHttpMessageConverter<Source>()); this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (jaxb2Present) { this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { this.messageConverters.add(new MappingJackson2HttpMessageConverter()); } else if (gsonPresent) { this.messageConverters.add(new GsonHttpMessageConverter()); } processor = new RequestResponseBodyMethodProcessor(this.messageConverters); } /** * This method will convert the response body to the desire format. */ public void handle(Object returnValue, HttpServletRequest request, HttpServletResponse response) throws Exception { ServletWebRequest nativeRequest = new ServletWebRequest(request, response); processor.handleReturnValue(returnValue, null, new ModelAndViewContainer(), nativeRequest); } /** * @return list of message converters */ public List<HttpMessageConverter<?>> getMessageConverters() { return messageConverters; }}
Step 2 - Create AuthenticationEntryPoint
As in many tutorials, this class is essential to implement custom error handling.
public class CustomEntryPoint implements AuthenticationEntryPoint { // The class from Step 1 private MessageProcessor processor; public CustomEntryPoint() { // It is up to you to decide when to instantiate processor = new MessageProcessor(); } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // This object is just like the model class, // the processor will convert it to appropriate format in response body CustomExceptionObject returnValue = new CustomExceptionObject(); try { processor.handle(returnValue, request, response); } catch (Exception e) { throw new ServletException(); } }}
Step 3 - Register the entry point
As mentioned, I do it with Java Config. I just show the relevant configuration here, there should be other configuration such as session stateless, etc.
@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint()); }}
Try with some authentication fail cases, remember the request header should include Accept : XXX and you should get the exception in JSON, XML or some other formats.
The best way I've found is to delegate the exception to the HandlerExceptionResolver
@Component("restAuthenticationEntryPoint")public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired private HandlerExceptionResolver resolver; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { resolver.resolveException(request, response, null, exception); }}
then you can use @ExceptionHandler to format the response the way you want.