I am developing a REST service. It uses JSON and must return some predefined JSON object in case of a problems. The default Spring response looks like this:
{
"timestamp": 1512578593776,
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/swagger-ui.html"
}
I want to replace this default JSON with an own one (with a stacktrace and additional exception related information).
Spring provides a handy way to overwrite default behavior. One should define a @RestControllerAdvice
bean with a custom exception handler. Like this
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<ExceptionResponse> unknownException(Exception ex) {
ExceptionResponse resp = new ExceptionResponse(ex, level); // my custom response object
return new ResponseEntity<ExceptionResponse>(resp, resp.getStatus());
}
@ExceptionHandler(value = {AuthenticationException.class})
public ResponseEntity<ExceptionResponse> authenticationException(AuthenticationExceptionex) {
// WON'T WORK
}
}
The custom ExceptionResponse
object will be then converted to JSON by Spring using a special message converter.
THE PROBLEM IS, that security exceptions like InsufficientAuthenticationException
cannot be intercepted by a method annotated as @ExceptionHandler
. This sort of exceptions happens before the Spring MVC dispatcher servlet were entered and all the MVC handlers were initialized.
It is possible to intercept this exception using a custom filter and build an own JSON serialization from scratch. In this case one would get a code which is completely independent from the rest of the Spring MVC infrastructure. It is not good.
The solution I found seems to work, but it looks crazy.
@Configuration
public class CustomSecurityConfiguration extends
WebSecurityConfigurerAdapter {
@Autowired
protected RequestMappingHandlerAdapter requestMappingHandlerAdapter;
@Autowired
protected GlobalExceptionHandler exceptionHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.fullyAuthenticated();
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
}
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
try {
ResponseEntity<ExceptionResponse> objResponse = exceptionHandler.authenticationException(authException);
Method unknownException = exceptionHandler.getClass().getMethod("authenticationException", AuthenticationException.class);
HandlerMethod handlerMethod = new HandlerMethod(exceptionHandler, unknownException);
MethodParameter returnType = handlerMethod.getReturnValueType(objResponse);
ModelAndViewContainer mvc = new ModelAndViewContainer(); // not really used here.
List<HttpMessageConverter<?>> mconverters = requestMappingHandlerAdapter.getMessageConverters();
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(mconverters);
processor.handleReturnValue(objResponse, returnType, mvc, webRequest);
} catch (IOException e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ServletException(e);
}
}
};
}
Is there a way to use the Spring serialization pipe (with Spring build in message converters, MIME format negotiation etc.) which looks better than this?
Create Bean Class
@Component public class AuthenticationExceptionHandler implements AuthenticationEntryPoint, Serializable
and override commence() method
and create a json response using object mapper like the following example
ObjectMapper mapper = new ObjectMapper();
String responseMsg = mapper.writeValueAsString(responseObject);
response.getWriter().write(responseMsg);
and @Autowire AuthenticationExceptionHandler
in SecurityConfiguration
class and in configure(HttpSecurity http) method
add below lines
http.exceptionHandling()
.authenticationEntryPoint(authenticationExceptionHandler)
This way, you should be able to send custom json response 401 /403. Same as above, you may use AccessDeniedHandler
. Let us know if this helped you solve your problem.
Spring provide out-of-box support for exception handling via AccessDeniedHandler
, which can be utilized by following below steps to achieve a custom JSON response in case of an AccessDeniedException
i.e. HTTP 403
Implement a custom handler something like below
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exc)
throws IOException, ServletException {
System.out.println("Access denied .. ");
// do something
response.sendRedirect("/deny");
}
}
Next create a bean of this handler in config and supply it to spring security exception handler (Important note - ensure to exclude /deny
from authentication else request will infinitely keep on resolving error)
@Bean
public AccessDeniedHandler accessDeniedHandler(){
return new CustomAccessDeniedHandler();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
// some other configuration
.antMatchers("/deny").permitAll()
.and()
.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}
Next in the controller class write a handler corresponding to /deny
and simply throw a new instance of SomeException
(or any other Exception
as suited) which will be intercepted in @RestControllerAdvice
respective handler.
@GetMapping("/deny")
public void accessDenied(){
throw new SomeException("User is not authorized");
}
Let know in comments if any more information is required.
I think the next configuration should work, please try it.
@Autowired
private HandlerExceptionResolver handlerExceptionResolver;
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
try {
handlerExceptionResolver.resolveException(request, response, null, authException);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ServletException(e);
}
}
};
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With