it-swarm.com.de

Behandeln Sie Authentifizierungsausnahmen für die Frühjahrssicherheit mit @ExceptionHandler

Ich verwende Spring MVCs @ControllerAdvice und @ExceptionHandler, um alle Ausnahmen einer REST - API zu behandeln. Es funktioniert gut für Ausnahmen, die von Web-MVC-Controllern ausgelöst werden, aber es funktioniert nicht für Ausnahmen, die von benutzerdefinierten Federsicherheitsfiltern ausgelöst werden, da sie ausgeführt werden, bevor die Controller-Methoden aufgerufen werden.

Ich habe einen benutzerdefinierten Federsicherheitsfilter, der eine tokenbasierte Authentifizierung ausführt:

public class AegisAuthenticationFilter extends GenericFilterBean {

...

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        try {

            ...         
        } catch(AuthenticationException authenticationException) {

            SecurityContextHolder.clearContext();
            authenticationEntryPoint.commence(request, response, authenticationException);

        }

    }

}

Mit diesem benutzerdefinierten Einstiegspunkt:

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{

    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
    }

}

Und mit dieser Klasse, um Ausnahmen global zu behandeln:

@ControllerAdvice
public class RestEntityResponseExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ InvalidTokenException.class, AuthenticationException.class })
    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    @ResponseBody
    public RestError handleAuthenticationException(Exception ex) {

        int errorCode = AegisErrorCode.GenericAuthenticationError;
        if(ex instanceof AegisException) {
            errorCode = ((AegisException)ex).getCode();
        }

        RestError re = new RestError(
            HttpStatus.UNAUTHORIZED,
            errorCode, 
            "...",
            ex.getMessage());

        return re;
    }
}

Was ich tun muss, ist, einen ausführlichen JSON-Body selbst für die Frühjahrssicherheit AuthenticationException zurückzugeben. Gibt es eine Möglichkeit, Spring Security AuthenticationEntryPoint und spring mvc @ExceptionHandler zusammenarbeiten zu lassen?

Ich verwende Federsicherheit 3.1.4 und Spring MVC 3.2.4.

66
Nicola

Ok, ich habe versucht, den Json selbst aus AuthenticationEntryPoint zu schreiben, und es funktioniert.

Nur zum Testen habe ich den AutenticationEntryPoint durch Entfernen von response.sendError geändert

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

    }
}

Auf diese Weise können Sie benutzerdefinierte Json-Daten zusammen mit dem 401 unautorisiert senden, auch wenn Sie Spring Security AuthenticationEntryPoint verwenden.

Offensichtlich würden Sie den Json nicht wie ich zu Testzwecken bauen, sondern eine Klasseninstanz serialisieren.

41
Nicola

Dies ist ein sehr interessantes Problem, dass das Spring Security - und Spring Web - Framework in der Art, wie sie mit der Antwort umgehen, nicht ganz konsistent ist. Ich glaube, es muss die Verarbeitung von Fehlernachrichten mit MessageConverter auf eine praktische Weise unterstützen.

Ich habe versucht, einen eleganten Weg zu finden, um MessageConverter in Spring Security einzuspritzen, damit sie die Ausnahme abfangen und sie gemäß den Inhaltsverhandlungen in einem richtigen Format zurückgeben können . Dennoch ist meine Lösung unten nicht elegant, aber zumindest Springcode verwenden.

Ich gehe davon aus, dass Sie wissen, wie man Jackson und die JAXB-Bibliothek einbindet, ansonsten gibt es keinen Grund, fortzufahren. Es gibt insgesamt 3 Schritte.

Schritt 1 - Erstellen Sie eine eigenständige Klasse, in der MessageConverter gespeichert werden

Diese Klasse spielt keine Magie. Es speichert einfach die Nachrichtenkonverter und einen Prozessor RequestResponseBodyMethodProcessor. Die Magie liegt in diesem Prozessor, der die gesamte Arbeit erledigt, einschließlich der Inhaltsverhandlung und der entsprechenden Umwandlung des Antwortkörpers.

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

Schritt 2 - Erstellen Sie AuthenticationEntryPoint

Wie in vielen Lernprogrammen ist diese Klasse für die Implementierung einer benutzerdefinierten Fehlerbehandlung unerlässlich.

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

Schritt 3 - Registrieren Sie den Einstiegspunkt

Wie gesagt, mache ich das mit Java Config. Ich zeige nur die relevante Konfiguration hier an, es sollte andere Konfiguration geben, z. B. Sitzung stateless usw.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
    }
}

Versuchen Sie es mit einigen Fällen von Authentifizierungsfehlern. Denken Sie daran, dass der Anforderungsheader Accept: XXX enthalten sollte, und Sie sollten die Ausnahme in JSON, XML oder einigen anderen Formaten erhalten.

32
Victor Wong

Der beste Weg, den ich gefunden habe, ist, die Ausnahme an den HandlerExceptionResolver zu delegieren

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

dann können Sie @ExceptionHandler verwenden, um die Antwort wie gewünscht zu formatieren.

12

Bei Spring Boot und @EnableResourceServer ist es relativ einfach und bequem, ResourceServerConfigurerAdapter anstelle von WebSecurityConfigurerAdapter in der Java-Konfiguration zu erweitern und ein benutzerdefiniertes AuthenticationEntryPoint zu registrieren, indem Sie configure(ResourceServerSecurityConfigurer resources) überschreiben und verwenden resources.authenticationEntryPoint(customAuthEntryPoint()) innerhalb der Methode. 

Etwas wie das:

@Configuration
@EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(customAuthEntryPoint());
    }

    @Bean
    public AuthenticationEntryPoint customAuthEntryPoint(){
        return new AuthFailureHandler();
    }
}

Es gibt auch ein Nice OAuth2AuthenticationEntryPoint, das erweitert werden kann (da es nicht endgültig ist) und bei der Implementierung eines benutzerdefinierten AuthenticationEntryPoint teilweise wiederverwendet werden kann. Insbesondere werden "WWW-Authenticate" -Header mit fehlerbezogenen Details hinzugefügt.

Hoffe das hilft jemandem. 

4
Vladimir Salin

Antworten von @Nicola und @Victor Wing nehmen und einen standardisierteren Weg hinzufügen:

import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import Java.io.IOException;

public class UnauthorizedErrorAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {

    private HttpMessageConverter messageConverter;

    @SuppressWarnings("unchecked")
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        MyGenericError error = new MyGenericError();
        error.setDescription(exception.getMessage());

        ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
        outputMessage.setStatusCode(HttpStatus.UNAUTHORIZED);

        messageConverter.write(error, null, outputMessage);
    }

    public void setMessageConverter(HttpMessageConverter messageConverter) {
        this.messageConverter = messageConverter;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        if (messageConverter == null) {
            throw new IllegalArgumentException("Property 'messageConverter' is required");
        }
    }

}

Jetzt können Sie konfigurierte Jackson-, Jaxb- oder andere Elemente zum Konvertieren von Antwortkörpern in Ihrer MVC-Annotation oder XML-basierten Konfiguration mit ihren Serialisierern, Deserialisierern usw. einfügen.

3

Ich konnte das umgehen, indem ich einfach die Methode "erfolglose Authentifizierung" in meinem Filter überschreibe. Dort sende ich eine Fehlerantwort mit dem gewünschten HTTP-Statuscode an den Client.

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException failed) throws IOException, ServletException {

    if (failed.getCause() instanceof RecordNotFoundException) {
        response.sendError((HttpServletResponse.SC_NOT_FOUND), failed.getMessage());
    }
}
0
user3619911

Ich verwende den objectMapper. Jeder Rest Service arbeitet hauptsächlich mit json, und in einer Ihrer Konfigurationen haben Sie bereits einen Objekt-Mapper konfiguriert.

Code ist in Kotlin geschrieben, hoffentlich wird es in Ordnung sein.

@Bean
fun objectMapper(): ObjectMapper {
    val objectMapper = ObjectMapper()
    objectMapper.registerModule(JodaModule())
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)

    return objectMapper
}

class UnauthorizedAuthenticationEntryPoint : BasicAuthenticationEntryPoint() {

    @Autowired
    lateinit var objectMapper: ObjectMapper

    @Throws(IOException::class, ServletException::class)
    override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
        response.addHeader("Content-Type", "application/json")
        response.status = HttpServletResponse.SC_UNAUTHORIZED

        val responseError = ResponseError(
            message = "${authException.message}",
        )

        objectMapper.writeValue(response.writer, responseError)
     }}
0
Spin1987

Update: Wenn Sie den Code direkt sehen möchten, habe ich zwei Beispiele für Sie, eines mit Standard Spring Security , der andere verwendet das Äquivalent zu Reactive Web und Reactive Security:
- Normales Web + Jwt-Sicherheit
- Reactive Jwt

Das, das ich immer für meine JSON-basierten Endpunkte verwende, sieht folgendermaßen aus:

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    ObjectMapper mapper;

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException e)
            throws IOException, ServletException {
        // Called when the user tries to access an endpoint which requires to be authenticated
        // we just return unauthorizaed
        logger.error("Unauthorized error. Message - {}", e.getMessage());

        ServletServerHttpResponse res = new ServletServerHttpResponse(response);
        res.setStatusCode(HttpStatus.UNAUTHORIZED);
        res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes());
    }
}

Der Object Mapper ist bereits eine Bean, nachdem Sie den Spring Web Starter hinzugefügt haben. Ich bevorzuge es, ihn anzupassen.

  @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.modules(new JavaTimeModule());

        // for example: Use created_at instead of createdAt
        builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

        // skip null fields
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return builder;
    }

Der AuthenticationEntryPoint, den Sie in Ihrer WebSecurityConfigurerAdapter-Klasse als Standard festgelegt haben:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ............
   @Autowired
    private JwtAuthEntryPoint unauthorizedHandler;
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // .antMatchers("/api/auth**", "/api/login**", "**").permitAll()
                .anyRequest().permitAll()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);


        http.headers().frameOptions().disable(); // otherwise H2 console is not available
        // There are many ways to ways of placing our Filter in a position in the chain
        // You can troubleshoot any error enabling debug(see below), it will print the chain of Filters
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
// ..........
}
0
Melardev

Wir müssen in diesem Fall HandlerExceptionResolver verwenden.

@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    //@Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        resolver.resolveException(request, response, null, authException);
    }
}

Außerdem müssen Sie der Exception-Handler-Klasse hinzufügen, um Ihr Objekt zurückzugeben.

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(AuthenticationException.class)
    public GenericResponseBean handleAuthenticationException(AuthenticationException ex, HttpServletResponse response){
        GenericResponseBean genericResponseBean = GenericResponseBean.build(MessageKeys.UNAUTHORIZED);
        genericResponseBean.setError(true);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        return genericResponseBean;
    }
}

möglicherweise erhalten Sie zum Zeitpunkt der Ausführung eines Projekts einen Fehler aufgrund mehrerer Implementierungen von HandlerExceptionResolver. In diesem Fall müssen Sie @Qualifier("handlerExceptionResolver") zu HandlerExceptionResolver hinzufügen.

0
Vinit Solanki