restcontrolleradvice manejo excepciones error ejemplo controlleradvice spring spring-mvc spring-security

spring - manejo - Gestione las excepciones de autenticación de seguridad de primavera con @ExceptionHandler



restcontrolleradvice (6)

Estoy usando @ControllerAdvice Spring MVC y @ExceptionHandler para manejar todas las excepciones de una API REST. Funciona bien para las excepciones lanzadas por los controladores mvc web, pero no funciona para las excepciones lanzadas por los filtros personalizados de seguridad de primavera porque se ejecutan antes de que se invoquen los métodos del controlador.

Tengo un filtro de seguridad de primavera personalizado que hace una autenticación basada en token:

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

Con este punto de entrada personalizado:

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

Y con esta clase para manejar excepciones globalmente:

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

Lo que tengo que hacer es devolver un cuerpo JSON detallado incluso para la seguridad de primavera AuthenticationException. ¿Hay alguna manera de que la seguridad de primavera AuthenticationEntryPoint y spring mvc @ExceptionHandler funcionen juntas?

Estoy usando spring security 3.1.4 y spring mvc 3.2.4.


En el caso de Spring Boot y @EnableResourceServer , es relativamente fácil y conveniente extender ResourceServerConfigurerAdapter lugar de WebSecurityConfigurerAdapter en la configuración de Java y registrar un AuthenticationEntryPoint al anular configure(ResourceServerSecurityConfigurer resources) y usar resources.authenticationEntryPoint(customAuthEntryPoint()) dentro del método .

Algo como esto:

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

También hay un buen OAuth2AuthenticationEntryPoint que se puede extender (ya que no es definitivo) y parcialmente reutilizado al implementar un AuthenticationEntryPoint personalizado. En particular, agrega encabezados "WWW-Authenticate" con detalles relacionados con errores.

Espero que esto ayude a alguien.


Este es un problema muy interesante que Spring Security y Spring Web Framework no son del todo consistentes en la forma en que manejan la respuesta. Creo que tiene que admitir de forma nativa el manejo de mensajes de error con MessageConverter de una manera práctica.

Traté de encontrar una forma elegante de inyectar MessageConverter en Spring Security para que pudieran detectar la excepción y devolverla en un formato correcto de acuerdo con la negociación de contenido . Aún así, mi solución a continuación no es elegante, pero al menos hace uso del código Spring.

Supongo que sabes cómo incluir la biblioteca de Jackson y JAXB; de lo contrario, no tiene sentido continuar. Hay 3 pasos en total.

Paso 1: crea una clase independiente, almacenando MessageConverters

Esta clase no juega magia. Simplemente almacena los convertidores de mensajes y un procesador RequestResponseBodyMethodProcessor . La magia está dentro de ese procesador que hará todo el trabajo, incluida la negociación de contenido y la conversión del cuerpo de respuesta en consecuencia.

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

Paso 2: crea AuthenticationEntryPoint

Como en muchos tutoriales, esta clase es esencial para implementar el manejo de errores personalizados.

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

Paso 3 - Registra el punto de entrada

Como mencioné, lo hago con Java Config. Solo muestro aquí la configuración relevante, debe haber otra configuración como sesión sin estado , etc.

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

Pruebe con algunos casos de fallas de autenticación, recuerde que el encabezado de la solicitud debe incluir Aceptar: XXX y debe obtener la excepción en JSON, XML u otros formatos.


Estoy usando ObjectMapper. Every Rest Service trabaja principalmente con json, y en una de sus configuraciones ya ha configurado un asignador de objetos.

El código está escrito en Kotlin, con suerte estará bien.

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


La mejor forma que he encontrado es delegar la excepción al 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); } }

luego puede usar @ExceptionHandler para formatear la respuesta de la manera que desee.


Ok, intenté como he sugerido escribir json yo mismo desde AuthenticationEntryPoint y funciona.

Solo para probar cambié el AutenticationEntryPoint eliminando response.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() + "/" }"); } }

De esta forma, puede enviar datos json personalizados junto con el 401 no autorizado incluso si está utilizando Spring Security AuthenticationEntryPoint.

Obviamente, no construirías el JSON como lo hice para fines de prueba, sino que serializarías una instancia de clase.


Tomando respuestas de @Nicola y @Victor Wing y agregando una forma más estandarizada:

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

Ahora puede inyectar Jackson, Jaxb configurado o lo que sea que use para convertir cuerpos de respuesta en su anotación MVC o configuración basada en XML con sus serializadores, deserializadores, etc.