Обрабатывать исключения проверки подлинности 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.
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")onHandlerExceptionResolver
Comments