web-dev-qa-db-de.com

Spring MVC (oder Spring Boot). Benutzerdefinierte JSON-Antwort für sicherheitsrelevante Ausnahmen wie 401 Unauthorized oder 403 Forbidden)

Ich entwickle einen REST Service. Es verwendet JSON und muss bei Problemen ein vordefiniertes JSON-Objekt zurückgeben. Die Standardantwort von Spring sieht folgendermaßen aus:

{
  "timestamp": 1512578593776,
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/swagger-ui.html"
}

Ich möchte diese Standard-JSON durch eine eigene ersetzen (mit einem Stacktrace und zusätzlichen Informationen zu Ausnahmen).

Spring bietet eine praktische Möglichkeit, das Standardverhalten zu überschreiben. Man sollte eine @RestControllerAdvice-Bean mit einem benutzerdefinierten Ausnahmehandler definieren. So was

@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
  }
}

Das benutzerdefinierte ExceptionResponse-Objekt wird dann von Spring mithilfe eines speziellen Nachrichtenkonverters in JSON konvertiert.

DAS PROBLEM IS , dass Sicherheitsausnahmen wie InsufficientAuthenticationExceptionkann nicht von einer als @ExceptionHandler annotierten Methode abgefangen werden. Diese Art von Ausnahmen tritt auf, bevor das Spring MVC-Dispatcher-Servlet eingegeben und alle MVC-Handler initialisiert wurden.

Es ist möglich, diese Ausnahme mit einem benutzerdefinierten Filter abzufangen und eine eigene JSON-Serialisierung von Grund auf zu erstellen. In diesem Fall würde man einen Code erhalten, der völlig unabhängig von der übrigen Spring MVC-Infrastruktur ist. Es ist nicht gut.

Die Lösung, die ich gefunden habe, scheint zu funktionieren, aber sie sieht verrückt aus.

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

Gibt es eine Möglichkeit, das Spring-Serialisierungsrohr (mit Spring-eingebauten Nachrichtenkonvertern, MIME-Formatverhandlungen usw.) zu verwenden, das besser aussieht?

8
30thh

Bean-Klasse erstellen 

@Component public class AuthenticationExceptionHandler implements AuthenticationEntryPoint, Serializable

und überschreiben Sie commence() method und erstellen Sie eine Json-Antwort, indem Sie den Object Mapper wie im folgenden Beispiel verwenden 

 ObjectMapper mapper = new ObjectMapper();
 String responseMsg = mapper.writeValueAsString(responseObject);
 response.getWriter().write(responseMsg);

und @Autowire AuthenticationExcetionHandler in SecurityConfiguration-Klasse und in configure(HttpSecurity http) method fügen Sie die folgenden Zeilen hinzu

        http.exceptionHandling()
            .authenticationEntryPoint(authenticationExceptionHandler) 

Auf diese Weise sollten Sie in der Lage sein, die benutzerdefinierte Antwort 401/403 zu senden. Wie oben können Sie auch AccessDeniedHandler verwenden. Teilen Sie uns mit, ob dies zur Lösung des Problems beigetragen hat. 

2
sandeep pandey

Ich denke, die nächste Konfiguration sollte funktionieren, bitte probieren Sie es aus.

@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 bietet Out-of-Box-Unterstützung für die Ausnahmebehandlung über AccessDeniedHandler , die durch folgende Schritte verwendet werden kann, um im Falle einer AccessDeniedException eine benutzerdefinierte JSON-Antwort zu erzielen, d. H. HTTP 403

Implementieren Sie einen benutzerdefinierten Handler wie unten beschrieben

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

Erstellen Sie als Nächstes ein Bean dieses Handlers in config und stellen Sie es dem Spring-Sicherheits-Ausnahmehandler bereit ( Wichtiger Hinweis - stellen Sie sicher, dass /deny von der Authentifizierung ausgeschlossen wird, andernfalls wird die Anforderung endlos weiter behoben)

@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());
}

Als nächstes schreiben Sie in die Controller-Klasse einen Handler, der /deny entspricht, und wirft einfach eine neue Instanz von SomeException (oder eine beliebige andere Exception als geeignet) aus, die in @RestControllerAdvice des jeweiligen Handlers abgefangen wird.

@GetMapping("/deny")
public void accessDenied(){
    throw new SomeException("User is not authorized");
}

Teilen Sie in Kommentaren mit, ob weitere Informationen erforderlich sind.

0