Обрабатывать исключения проверки подлинности spring security с помощью @ExceptionHandler



Я использую Spring MVC @ControllerAdvice и @ExceptionHandler для обработки всех исключений REST Api. Он отлично работает для исключений, создаваемых контроллерами web mvc, но он не работает для исключений, создаваемых пользовательскими фильтрами spring security, потому что они запускаются до вызова методов контроллера.



у меня есть пользовательский фильтр безопасности spring, который выполняет аутентификацию на основе маркера:



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

}

}

}


С этой нестандартной точкой входа:



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

}


и с этим классом справиться исключения глобально:



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


что мне нужно сделать, это вернуть подробное тело JSON даже для spring security AuthenticationException. Есть ли способ заставить spring security AuthenticationEntryPoint и spring mvc @ExceptionHandler работать вместе?



я использую spring security 3.1.4 и spring mvc 3.2.4.

1142   7  

7 ответов:

хорошо, я попробовал, как предложил написать json сам из AuthenticationEntryPoint, и это работает.

только для тестирования я изменил AutenticationEntryPoint, удалив ответ.sendError

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

    }
}

таким образом, вы можете отправлять пользовательские данные json вместе с 401 несанкционированным, даже если вы используете Spring Security AuthenticationEntryPoint.

очевидно, что вы не построили бы json, как я сделал для целей тестирования, но вы бы сериализовали какой-то класс пример.

это очень интересная проблема, что Spring Security и Spring Web рамки не совсем последовательны в том, как они обрабатывают ответ. Я считаю, что он должен изначально поддерживать обработку сообщений об ошибках с помощью MessageConverter в удобном виде.

Я пытался найти элегантный способ ввести MessageConverter в весеннюю безопасность, чтобы они могли поймать исключение и верните их в правильном формате в соответствии с содержанием переговоров. Тем не менее, мой Решение ниже не является элегантным, но, по крайней мере, использовать код весны.

Я предполагаю, что вы знаете, как включить библиотеку Джексона и JAXB, иначе нет смысла продолжать. Есть 3 шага в общей сложности.

Шаг 1-Создайте автономный класс, хранящий MessageConverters

этот класс не играет никакой магии. Он просто хранит конвертеры сообщений и процессор RequestResponseBodyMethodProcessor. Магия находится внутри этого процессора, который будет выполнять всю работу, включая согласование контента и преобразование тела ответа соответственно.

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

Шаг 2-Создать AuthenticationEntryPoint

как и во многих учебниках, этот класс необходим для реализации пользовательской обработки ошибок.

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

Шаг 3-Регистрация точки входа

как уже упоминалось, я делаю это с помощью Java Config. Я просто показываю соответствующую конфигурацию здесь, должна быть другая конфигурация, такая как session и т. д.

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

попробовать в некоторых случаях сбоя проверки подлинности помните, что заголовок запроса должен включать принять: XXX и вы должны получить исключение в JSON, XML или другие форматы.

лучший способ, который я нашел, это делегировать исключение HandlerExceptionResolver

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

затем вы можете использовать @ExceptionHandler для форматирования ответа так, как вы хотите.

принимая ответы от @Nicola и @Victor Wing и добавляя более стандартизированный способ:

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

}

теперь вы можете ввести сконфигурированный Jackson, Jaxb или все, что вы используете для преобразования тел ответов в вашей аннотации MVC или конфигурации на основе XML с ее сериализаторами, десериализаторами и т. д.

в случае пружинного ботинка и @EnableResourceServer, Это относительно легко и удобно для расширения ResourceServerConfigurerAdapter вместо WebSecurityConfigurerAdapter в конфигурации Java и зарегистрировать пользовательский AuthenticationEntryPoint переопределив configure(ResourceServerSecurityConfigurer resources) и с помощью resources.authenticationEntryPoint(customAuthEntryPoint()) внутри метода.

что-то вроде этого:

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

еще OAuth2AuthenticationEntryPoint который может быть расширен (так как он не является окончательным) и частично повторно использован при реализации пользовательского AuthenticationEntryPoint. В частности, он добавляет заголовки "WWW-Authenticate" с сведения, связанные с ошибкой.

надеюсь, что это поможет кому-то.

Я использую objectMapper. Каждая служба Rest в основном работает с json, и в одном из ваших конфигураций вы уже настроили сопоставитель объектов.

код написан в Котлине, надеюсь, все будет в порядке.

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

мы должны использовать HandlerExceptionResolver в этом случае.

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

кроме того, вам нужно добавить в класс обработчика исключений, чтобы вернуть ваш объект.

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

вы можете получить ошибку во время запуска проекта из-за нескольких реализаций HandlerExceptionResolver в этом случае вы должны добавить @Qualifier("handlerExceptionResolver") on HandlerExceptionResolver

Comments

    Ничего не найдено.