web-dev-qa-db-fra.com

Spring MVC (ou Spring Boot). Réponse JSON personnalisée pour les exceptions liées à la sécurité telles que 401 non autorisé ou 403 interdit

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?

8
30thh

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. 

2
sandeep pandey

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);
            }
        }
    };
}
1

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.

0
Bond - Java Bond