Je développe un service REST. Il utilise JSON et doit renvoyer un objet JSON prédéfini en cas de problème. La réponse par défaut de Spring ressemble à ceci:
{
"timestamp": 1512578593776,
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/swagger-ui.html"
}
Je souhaite remplacer ce JSON par défaut par un propre (avec un stacktrace et des informations supplémentaires relatives aux exceptions).
Spring fournit un moyen pratique d’écraser le comportement par défaut. Il faut définir un bean @RestControllerAdvice
avec un gestionnaire d'exceptions personnalisé. Comme ça
@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
}
}
L'objet ExceptionResponse
personnalisé sera ensuite converti en JSON par Spring à l'aide d'un convertisseur de message spécial.
LE PROBLÈME IS , le fait que des exceptions de sécurité telles que InsufficientAuthenticationException
ne puisse pas être intercepté par une méthode annotée comme @ExceptionHandler
. Ce type d'exception se produit avant la saisie du servlet de répartiteur Spring MVC et l'initialisation de tous les gestionnaires MVC.
Il est possible d'intercepter cette exception à l'aide d'un filtre personnalisé et de créer une sérialisation JSON propre à partir de zéro. Dans ce cas, on obtiendrait un code complètement indépendant du reste de l'infrastructure Spring MVC. Ce n'est pas bon.
La solution que j'ai trouvée semble fonctionner, mais elle a l'air folle.
@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);
}
}
};
}
Existe-t-il un moyen d'utiliser le canal de sérialisation Spring (avec les convertisseurs de messages intégrés à Spring, la négociation de format MIME, etc.) plus esthétique?
Créer une classe de haricot
@Component public class AuthenticationExceptionHandler implements AuthenticationEntryPoint, Serializable
et écrasez commence() method
et créez une réponse Json en utilisant Object Mapper comme dans l'exemple ci-dessous
ObjectMapper mapper = new ObjectMapper();
String responseMsg = mapper.writeValueAsString(responseObject);
response.getWriter().write(responseMsg);
et @Autowire AuthenticationExcetionHandler
dans SecurityConfiguration
classe et dans configure(HttpSecurity http) method
ajouter des lignes ci-dessous
http.exceptionHandling()
.authenticationEntryPoint(authenticationExceptionHandler)
De cette façon, vous devriez pouvoir envoyer une réponse custome json 401/403. comme ci-dessus, vous pouvez utiliser AccessDeniedHandler
. Faites-nous savoir si cela vous a aidé à résoudre le problème.
Je pense que la prochaine configuration devrait fonctionner, veuillez l'essayer.
@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);
}
}
};
}
Spring fournit un support immédiat pour la gestion des exceptions via AccessDeniedHandler
, qui peut être utilisé en suivant les étapes ci-dessous pour obtenir une réponse JSON personnalisée dans le cas d'une AccessDeniedException
i.e. HTTP 403
Implémenter un gestionnaire personnalisé comme ci-dessous
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");
}
}
Créez ensuite un bean de ce gestionnaire dans config et fournissez-le au gestionnaire d'exception de sécurité ( Remarque importante - assurez-vous d'exclure /deny
de l'authentification. Sinon, la demande restera indéfiniment à la résolution des erreurs).
@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());
}
Ensuite, dans la classe du contrôleur, écrivez un gestionnaire correspondant à /deny
et lancez simplement une nouvelle instance de SomeException
(ou toute autre Exception
selon les besoins) qui sera interceptée dans le gestionnaire @RestControllerAdvice
respectif.
@GetMapping("/deny")
public void accessDenied(){
throw new SomeException("User is not authorized");
}
Faites savoir dans les commentaires si d'autres informations sont nécessaires.