Estoy usando Spring MVC @ControllerAdvice
y @ExceptionHandler
para manejar toda la excepción de una API REST. Funciona bien para las excepciones lanzadas por los controladores web mvc, 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 realiza 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 a nivel mundial:
@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 Spring Security AuthenticationException. ¿Hay alguna manera de hacer que Spring Security AuthenticationEntryPoint y spring mvc @ExceptionHandler funcionen juntos?
Estoy usando Spring Security 3.1.4 y Spring MVC 3.2.4.
fuente
(@)ExceptionHandler
solo funcionará si la solicitud es manejada porDispatcherServlet
. Sin embargo, esta excepción ocurre antes de eso, ya que es lanzada por unFilter
. Por lo tanto, nunca podrá manejar esta excepción con un(@)ExceptionHandler
.EntryPoint
. Es posible que desee construir el objeto allí e inyectar unMappingJackson2HttpMessageConverter
allí.Respuestas:
Ok, intenté como se sugirió escribir el json desde AuthenticationEntryPoint y funciona.
Solo para probar, cambié AutenticationEntryPoint eliminando response.sendError
De esta manera, puede enviar datos json personalizados junto con el 401 sin autorización incluso si está utilizando Spring Security AuthenticationEntryPoint.
Obviamente, no compilaría el json como lo hice yo con fines de prueba, pero serializaría alguna instancia de clase.
fuente
Este es un problema muy interesante de 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 devolverlos en el 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 sabe cómo incluir la biblioteca Jackson y JAXB, de lo contrario, no tiene sentido continuar. Hay 3 pasos en total.
Paso 1: crea una clase independiente, almacena MessageConverters
Esta clase no juega a la 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.Paso 2: crear AuthenticationEntryPoint
Como en muchos tutoriales, esta clase es esencial para implementar el manejo de errores personalizado.
Paso 3: registrar el punto de entrada
Como se mencionó, lo hago con Java Config. Solo muestro la configuración relevante aquí, debería haber otra configuración como sesión sin estado , etc.
Pruebe con algunos casos de error de autenticación, recuerde que el encabezado de la solicitud debe incluir Accept: XXX y debe obtener la excepción en JSON, XML o algunos otros formatos.
fuente
InvalidGrantException
pero mi versión de tuCustomEntryPoint
no se invoca. ¿Alguna idea de lo que podría estar perdiendo?AuthenticationEntryPoint
yAccessDeniedHandler
tales comoUsernameNotFoundException
yInvalidGrantException
pueden ser manejados porAuthenticationFailureHandler
como se explica aquí .La mejor manera que he encontrado es delegar la excepción al HandlerExceptionResolver
luego puede usar @ExceptionHandler para formatear la respuesta de la manera que desee.
fuente
@ControllerAdvice
no funcionará si ha especificado paquetes base en la anotación. Tuve que eliminar esto por completo para permitir que se llamara al controlador.@Component("restAuthenticationEntryPoint")
? ¿Por qué la necesidad de un nombre como restAuthenticationEntryPoint? ¿Es para evitar algunas colisiones de nombres de Spring?En el caso de Spring Boot y
@EnableResourceServer
, es relativamente fácil y conveniente extender enResourceServerConfigurerAdapter
lugar deWebSecurityConfigurerAdapter
en la configuración de Java y registrar una costumbreAuthenticationEntryPoint
anulandoconfigure(ResourceServerSecurityConfigurer resources)
y usandoresources.authenticationEntryPoint(customAuthEntryPoint())
dentro del método.Algo como esto:
También hay algo bueno
OAuth2AuthenticationEntryPoint
que puede extenderse (ya que no es definitivo) y reutilizarse parcialmente al implementar unAuthenticationEntryPoint
. En particular, agrega encabezados "WWW-Authenticate" con detalles relacionados con el error.Espero que esto ayude a alguien.
fuente
commence()
función de miAuthenticationEntryPoint
no se invoca, ¿me falta algo?Tomando respuestas de @Nicola y @Victor Wing y agregando una forma más estandarizada:
Ahora, puede inyectar Jackson configurado, Jaxb 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.
fuente
<property name="messageConverter" ref="myConverterBeanName"/>
etiqueta. Cuando use una@Configuration
clase, simplemente use elsetMessageConverter()
método.Necesitamos usar
HandlerExceptionResolver
en ese caso.Además, debe agregar la clase de controlador de excepciones para devolver su objeto.
puede obtener un error en el momento de la ejecución de un proyecto debido a múltiples implementaciones de
HandlerExceptionResolver
, En ese caso hay que añadir@Qualifier("handlerExceptionResolver")
elHandlerExceptionResolver
fuente
GenericResponseBean
es solo java pojo, puedes crear el tuyo propioPude manejar eso simplemente anulando el método 'unsuccessfulAuthentication' en mi filtro. Allí, envío una respuesta de error al cliente con el código de estado HTTP deseado.
fuente
Actualización: si te gusta y prefieres ver el código directamente, tengo dos ejemplos para ti, uno que usa Spring Security estándar, que es lo que estás buscando, el otro usa el equivalente de Reactive Web y Reactive Security:
- Normal Seguridad Web + Jwt
- Jwt reactivo
El que siempre uso para mis puntos finales basados en JSON se parece a lo siguiente:
El asignador de objetos se convierte en un bean una vez que agrega el iniciador web de primavera, pero prefiero personalizarlo, así que aquí está mi implementación para ObjectMapper:
El AuthenticationEntryPoint predeterminado que estableció en su clase WebSecurityConfigurerAdapter:
fuente
Personalice el filtro y determine qué tipo de anomalía, debería haber un método mejor que este
}
fuente
Estoy usando objectMapper. Cada servicio de descanso funciona principalmente con json, y en una de sus configuraciones ya ha configurado un mapeador de objetos.
El código está escrito en Kotlin, con suerte estará bien.
fuente
En
ResourceServerConfigurerAdapter
clase, el siguiente código recortado funcionó para mí.http.exceptionHandling().authenticationEntryPoint(new AuthFailureHandler()).and.csrf()..
no funcionó. Por eso lo escribí como una llamada separada.Implementación de AuthenticationEntryPoint para detectar el vencimiento del token y el encabezado de autorización faltante.
fuente