Ручка пружинный исключения проверки подлинности с @ExceptionHandler

Я'м с использованием Spring MVC, О'ы @ControllerAdvice и @ExceptionHandler обрабатывать все исключения из API-интерфейса REST. Он отлично работает для исключений, веб-контроллеры в MVC, но это не работает для исключений, весна пользовательские фильтры безопасности, поскольку они выполняются до методов контроллера вызываются.

У меня есть обычай весенней фильтр безопасности, что делает маркер на основе авт:

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 даже для весны AuthenticationException безопасности. Есть ли способ сделать весной AuthenticationEntryPoint безопасности и весна в MVC @ExceptionHandler работать вместе?

Я'м с использованием Spring безопасности 3.1.4 и весна в MVC 3.2.4.

Комментарии к вопросу (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 несанкционированного даже если вы используете Весна AuthenticationEntryPoint безопасности.

Очевидно, что вы не хотели построить JSON, как я сделал для тестирования, но вы бы сериализовать некоторые экземпляра класса.

Комментарии (3)

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

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

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

Шаг 1 - создать отдельный класс, хранение MessageConverters

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

public class MessageProcessor { // Any name you like
    // List of HttpMessageConverter
    private List> 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>();

        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        this.messageConverters.add(new ResourceHttpMessageConverter());
        this.messageConverters.add(new SourceHttpMessageConverter());
        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> 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 конфиг. Я просто показать соответствующие настройки здесь должны быть и другие конфигурации, такие как сессии апатриды и т. д.

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

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

Комментарии (1)

Лучший способ я'вэ нашел делегировать исключение из 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 в формат ответа, как вы хотите.

Комментарии (2)

В случае весенних ботинок и @EnableResourceServer, он относительно легкий и удобный, чтобы продлить ResourceServerConfigurerAdapter вместо WebSecurityConfigurerAdapter в Java конфигурации и зарегистрировать пользовательский AuthenticationEntryPoint путем переопределения настроить(ResourceServerSecurityConfigurer ресурсов) и использования ресурсов.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();
    }
}

Там's также хороший OAuth2AuthenticationEntryPoint, который может быть продлен (так как он's не окончательной) и частично повторно использован при реализации пользовательского AuthenticationEntryPoint. В частности, он добавляет назальный веб-аутентификации; заголовки с ошибками-подробности.

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

Комментарии (1)

Принимая ответы от @Никола и @Виктор крыла и добавив более унифицированной форме:

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

}

Теперь, вы можете вводить настроен Джексон, JAXB или что вы используете, чтобы преобразовать органов ответ на ваш MVC в аннотации или XML-конфигурацию в соответствии со своими сериализаторы, десериализаторы и так далее.

Комментарии (2)

Мы должны использовать 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, в этом случае вы должны добавить @квалификатор (на"handlerExceptionResolver", У) О HandlerExceptionResolver

Комментарии (1)

Мне удалось справиться путем простого переопределения метода 'unsuccessfulAuthentication' в мой фильтр. Нет, я отправить сообщение об ошибке клиенту с необходимый код состояния HTTP.

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

Я'м с помощью objectMapper. Каждое служение остальное в основном работает с 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)
     }}
Комментарии (0)

Обновление: если вы любите и предпочитаете, чтобы увидеть код напрямую, то у меня есть два примера для вас, используя стандартные весенние обеспечения которых является то, что вы ищете, другая-с использованием эквивалента реактивной веб и реактивной безопасности:<БР>

@Подгружен ObjectMapper картографа;

частная статические заключительные регистратор регистратор = LoggerFactory.getLogger(JwtAuthEntryPoint.class);

@Переопределить общественного недействительными начать(HttpServletRequest запросу Ответ HttpServletResponse, AuthenticationException е) бросает IOException, ServletException { // Вызывается, когда пользователь пытается получить доступ к конечной точке, которая требует проверки подлинности // мы просто вернем unauthorizaed логгер.ошибки("от несанкционированного ошибка. Сообщение - {} себе", Эл.метод GetMessage());

ServletServerHttpResponse рез = новый ServletServerHttpResponse(ответ); РЭС.setStatusCode(HttpStatus.Несанкционированный); РЭС.getServletResponse().действие setheader(HttpHeaders.CONTENT_TYPE, тип носителя.APPLICATION_JSON_VALUE); РЭС.getBody().писать(маппер.writeValueAsString(новый ErrorResponse("Вы должны аутентифицироваться на")).метод getbytes()); } } ``

Объектом сопоставления становится зернах после добавления весны веб-стартера, но я предпочитаю, чтобы настроить его, так вот моя реализация для ObjectMapper: ``Ява @Фасоль общественные Jackson2ObjectMapperBuilder objectMapperBuilder() { Jackson2ObjectMapperBuilder построитель = новый Jackson2ObjectMapperBuilder(); строитель.модули(новые JavaTimeModule());

// например: использовать created_at вместо createdAt строитель.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

// пропустить пустые поля строитель.serializationInclusion(JsonInclude.Включить.NON_NULL); строитель.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); возвращение строитель; } В AuthenticationEntryPoint по умолчанию установить в вашем классе WebSecurityConfigurerAdapter: Ява @Конфигурации @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = истина) общественный класс расширяет SecurityConfig WebSecurityConfigurerAdapter { // ............ @Подгружен частная JwtAuthEntryPoint unauthorizedHandler; @Переопределить охраняемых недействительными настроить(HttpSecurity по HTTP) бросает исключение { протоколу HTTP.CORS (с).и().CSRF атак().отключить() .authorizeRequests() // .antMatchers(" по - /по API/автто", " по - /по API/логини", " в**то").permitAll() .anyRequest().permitAll() .и() .обработка исключений().authenticationEntryPoint(unauthorizedHandler) .и() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.Без гражданства);

протоколу HTTP.заголовки().frameOptions().отключить(); // в противном случае Н2 консоль недоступна // Есть много способов способы размещения наш фильтр в цепи // Вы можете устранить любые ошибки включение отладки(см. ниже), он будет печатать цепочки фильтров протоколу HTTP.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } // .......... } ``

Комментарии (0)