Práctica recomendada para la autenticación basada en tokens REST con JAX-RS y Jersey

Estoy buscando una forma de habilitar la autenticación basada en tokens en Jersey. Estoy tratando de no usar ningún marco particular. ¿Es eso posible?

Mi plan es: un usuario se registra en mi servicio web, mi servicio web genera un token, lo envía al cliente y el cliente lo conservará. Luego, el cliente, para cada solicitud, enviará el token en lugar de nombre de usuario y contraseña.

Estaba pensando en utilizar un filtro personalizado para cada solicitud y @PreAuthorize("hasRole('ROLE')") pero pensé que esto causa una gran cantidad de solicitudes a la base de datos para comprobar si el token es válido.

¿O no crear filtro y en cada solicitud poner un token de param? De modo que cada API comprueba primero el token y luego ejecuta algo para recuperar el recurso.

Cómo funciona la autenticación basada en token

En la autenticación basada en token, el cliente intercambia credenciales (como el nombre de usuario y la contraseña) por una pieza de datos denominada token . Para cada solicitud, en lugar de enviar las credenciales duras, el cliente enviará el token al servidor para realizar la autenticación y luego la autorización.

En pocas palabras, un esquema de autenticación basado en tokens sigue estos pasos:

  1. El cliente envía sus credenciales (nombre de usuario y contraseña) al servidor.
  2. El servidor autentica las credenciales y, si son válidas, genera un token para el usuario.
  3. El servidor almacena el token generado anteriormente en algún almacenamiento junto con el identificador de usuario y una fecha de vencimiento.
  4. El servidor envía el token generado al cliente.
  5. El cliente envía el token al servidor en cada solicitud.
  6. El servidor, en cada solicitud, extrae el token de la solicitud entrante. Con el token, el servidor busca los detalles del usuario para realizar la autenticación.
    • Si el token es válido, el servidor acepta la solicitud.
    • Si el token no es válido, el servidor rechaza la solicitud.
  7. Una vez que se ha realizado la autenticación, el servidor realiza la autorización.
  8. El servidor puede proporcionar un punto final para actualizar los tokens.

Nota: El paso 3 no es necesario si el servidor ha emitido un token firmado (como JWT, que le permite realizar una autenticación sin estado ).

Qué puedes hacer con JAX-RS 2.0 (Jersey, RESTEasy y Apache CXF)

Esta solución utiliza solo la API JAX-RS 2.0, evitando cualquier solución específica del proveedor . Por lo tanto, debería funcionar con las implementaciones de JAX-RS 2.0, como Jersey , RESTEasy y Apache CXF .

Vale la pena mencionar que si está utilizando la autenticación basada en token, no está confiando en los mecanismos de seguridad de la aplicación web Java EE estándar ofrecidos por el contenedor de servlets y configurables a través del descriptor web.xml la aplicación. Es una autenticación personalizada.

Autenticar a un usuario con su nombre de usuario y contraseña y emitir un token

Cree un método de recurso JAX-RS que reciba y valide las credenciales (nombre de usuario y contraseña) y emita un token para el usuario:

 @Path("/authentication") public class AuthenticationEndpoint { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response authenticateUser(@FormParam("username") String username, @FormParam("password") String password) { try { // Authenticate the user using the credentials provided authenticate(username, password); // Issue a token for the user String token = issueToken(username); // Return the token on the response return Response.ok(token).build(); } catch (Exception e) { return Response.status(Response.Status.FORBIDDEN).build(); } } private void authenticate(String username, String password) throws Exception { // Authenticate against a database, LDAP, file or whatever // Throw an Exception if the credentials are invalid } private String issueToken(String username) { // Issue a token (can be a random String persisted to a database or a JWT token) // The issued token must be associated to a user // Return the issued token } } 

Si se producen excepciones al validar las credenciales, se devolverá una respuesta con el estado 403 (Prohibido).

Si las credenciales se validan satisfactoriamente, se devolverá una respuesta con el estado 200 (OK) y el token emitido se enviará al cliente en la carga de respuesta. El cliente debe enviar el token al servidor en cada solicitud.

Al consumir application/x-www-form-urlencoded , el cliente debe enviar las credenciales en el siguiente formato en la carga de solicitud:

 username=admin&password=123456 

En lugar de los parámetros de formulario, es posible incluir el nombre de usuario y la contraseña en una clase:

 public class Credentials implements Serializable { private String username; private String password; // Getters and setters omitted } 

Y luego consumirlo como JSON:

 @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Response authenticateUser(Credentials credentials) { String username = credentials.getUsername(); String password = credentials.getPassword(); // Authenticate the user, issue a token and return a response } 

Usando este enfoque, el cliente debe enviar las credenciales en el siguiente formato en la carga de la solicitud:

 { "username": "admin", "password": "123456" } 

Extraer el token de la solicitud y validarlo

El cliente debe enviar el token en el encabezado de Authorization HTTP estándar de la solicitud. Por ejemplo:

 Authorization: Bearer  

El nombre del encabezado HTTP estándar es desafortunado porque transporta información de autenticación , no de autorización . Sin embargo, es el encabezado HTTP estándar para enviar credenciales al servidor.

JAX-RS proporciona @NameBinding , una meta-anotación utilizada para crear otras anotaciones para enlazar filtros e interceptores a clases de recursos y métodos. Defina una anotación @Secured siguiente manera:

 @NameBinding @Retention(RUNTIME) @Target({TYPE, METHOD}) public @interface Secured { } 

La anotación de enlace de nombre definida anteriormente se usará para decorar una clase de filtro, que implementa ContainerRequestFilter , lo que le permite interceptar la solicitud antes de que sea manejada por un método de recurso. El ContainerRequestContext se puede usar para acceder a los encabezados de solicitud HTTP y luego extraer el token:

 @Secured @Provider @Priority(Priorities.AUTHENTICATION) public class AuthenticationFilter implements ContainerRequestFilter { private static final String REALM = "example"; private static final String AUTHENTICATION_SCHEME = "Bearer"; @Override public void filter(ContainerRequestContext requestContext) throws IOException { // Get the Authorization header from the request String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); // Validate the Authorization header if (!isTokenBasedAuthentication(authorizationHeader)) { abortWithUnauthorized(requestContext); return; } // Extract the token from the Authorization header String token = authorizationHeader .substring(AUTHENTICATION_SCHEME.length()).trim(); try { // Validate the token validateToken(token); } catch (Exception e) { abortWithUnauthorized(requestContext); } } private boolean isTokenBasedAuthentication(String authorizationHeader) { // Check if the Authorization header is valid // It must not be null and must be prefixed with "Bearer" plus a whitespace // The authentication scheme comparison must be case-insensitive return authorizationHeader != null && authorizationHeader.toLowerCase() .startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " "); } private void abortWithUnauthorized(ContainerRequestContext requestContext) { // Abort the filter chain with a 401 status code response // The WWW-Authenticate header is sent along with the response requestContext.abortWith( Response.status(Response.Status.UNAUTHORIZED) .header(HttpHeaders.WWW_AUTHENTICATE, AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"") .build()); } private void validateToken(String token) throws Exception { // Check if the token was issued by the server and if it's not expired // Throw an Exception if the token is invalid } } 

Si ocurre algún problema durante la validación del token, se devolverá una respuesta con el estado 401 (No autorizado). De lo contrario, la solicitud pasará a un método de recursos.

Asegurar tus puntos finales REST

Para enlazar el filtro de autenticación con los métodos de recursos o las clases de recursos, @Secured con la anotación @Secured creada anteriormente. Para los métodos y / o clases que están anotados, se ejecutará el filtro. Significa que dichos puntos finales solo se alcanzarán si la solicitud se realiza con un token válido.

Si algunos métodos o clases no necesitan autenticación, simplemente no los anote:

 @Path("/example") public class ExampleResource { @GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public Response myUnsecuredMethod(@PathParam("id") Long id) { // This method is not annotated with @Secured // The authentication filter won't be executed before invoking this method ... } @DELETE @Secured @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public Response mySecuredMethod(@PathParam("id") Long id) { // This method is annotated with @Secured // The authentication filter will be executed before invoking this method // The HTTP request must be performed with a valid token ... } } 

En el ejemplo que se muestra arriba, el filtro se ejecutará solo para el mySecuredMethod(Long) porque está anotado con @Secured .

Identificando al usuario actual

Es muy probable que necesite conocer al usuario que realiza la solicitud contra su API REST. Los siguientes enfoques se pueden usar para lograrlo:

Anulando el contexto de seguridad de la solicitud actual

Dentro de su método ContainerRequestFilter.filter(ContainerRequestContext) , se puede establecer una nueva instancia de SecurityContext para la solicitud actual. A continuación, anule SecurityContext.getUserPrincipal() y devuelva una instancia de Principal :

 final SecurityContext currentSecurityContext = requestContext.getSecurityContext(); requestContext.setSecurityContext(new SecurityContext() { @Override public Principal getUserPrincipal() { return () -> username; } @Override public boolean isUserInRole(String role) { return true; } @Override public boolean isSecure() { return currentSecurityContext.isSecure(); } @Override public String getAuthenticationScheme() { return AUTHENTICATION_SCHEME; } }); 

Use el token para buscar el identificador de usuario (nombre de usuario), que será el nombre del Principal .

Inyecte SecurityContext en cualquier clase de recurso JAX-RS:

 @Context SecurityContext securityContext; 

Lo mismo se puede hacer en un método de recursos JAX-RS:

 @GET @Secured @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public Response myMethod(@PathParam("id") Long id, @Context SecurityContext securityContext) { ... } 

Y luego consigue el Principal :

 Principal principal = securityContext.getUserPrincipal(); String username = principal.getName(); 

Uso de CDI (Inyección de Contexto e Dependencia)

Si, por alguna razón, no desea anular el SecurityContext , puede usar CDI (Inyección de contexto y dependencia), que proporciona funciones útiles, como eventos y productores.

Crear un calificador CDI:

 @Qualifier @Retention(RUNTIME) @Target({ METHOD, FIELD, PARAMETER }) public @interface AuthenticatedUser { } 

En su AuthenticationFilter creado arriba, @AuthenticatedUser un Event anotado con @AuthenticatedUser :

 @Inject @AuthenticatedUser Event userAuthenticatedEvent; 

Si la autenticación tiene éxito, active el evento pasando el nombre de usuario como parámetro (recuerde, el token se emite para un usuario y el token se utilizará para buscar el identificador de usuario):

 userAuthenticatedEvent.fire(username); 

Es muy probable que haya una clase que represente a un usuario en su aplicación. Llamemos a esta clase User .

Cree un bean CDI para manejar el evento de autenticación, encuentre una instancia de User con el nombre de usuario correspondiente y asígnela al campo de productor del usuario authenticatedUser :

 @RequestScoped public class AuthenticatedUserProducer { @Produces @RequestScoped @AuthenticatedUser private User authenticatedUser; public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) { this.authenticatedUser = findUser(username); } private User findUser(String username) { // Hit the the database or a service to find a user by its username and return it // Return the User instance } } 

El campo authenticatedUser produce una instancia de User que se puede inyectar en beans administrados por contenedor, como servicios JAX-RS, beans CDI, servlets y EJB. Utilice la siguiente pieza de código para inyectar una instancia de User (de hecho, es un proxy CDI):

 @Inject @AuthenticatedUser User authenticatedUser; 

Tenga en cuenta que la anotación CDI @Produces es diferente de la anotación JAX-RS @Produces :

  • CDI: javax.enterprise.inject.Produces
  • JAX-RS: javax.ws.rs.Produces

Asegúrese de utilizar la anotación CDI @Produces en su bean AuthenticatedUserProducer .

La clave aquí es el bean anotado con @RequestScoped , que le permite compartir datos entre los filtros y sus beans. Si no desea utilizar eventos, puede modificar el filtro para almacenar el usuario autenticado en un bean con ámbito de solicitud y luego leerlo de las clases de recursos de JAX-RS.

Comparado con el enfoque que anula el SecurityContext , el enfoque CDI le permite obtener el usuario autenticado de beans que no sean recursos ni proveedores de JAX-RS.

Apoyar la autorización basada en roles

Consulte mi otra respuesta para obtener detalles sobre cómo respaldar la autorización basada en roles.

Emisión de tokens

Un token puede ser:

  • Opaco: no revela detalles que no sean el valor en sí (como una cadena aleatoria)
  • Autónomo: contiene detalles sobre el token en sí (como JWT).

Vea los detalles abajo:

Cadena aleatoria como token

Se puede emitir un token generando una cadena aleatoria y manteniéndola en una base de datos junto con el identificador de usuario y una fecha de vencimiento. Un buen ejemplo de cómo generar una cadena aleatoria en Java se puede ver aquí . También podrías usar:

 Random random = new SecureRandom(); String token = new BigInteger(130, random).toString(32); 

JWT (JSON Token web)

JWT (JSON Web Token) es un método estándar para representar las reclamaciones de forma segura entre dos partes y está definido por el RFC 7519 .

Es un token autónomo y le permite almacenar detalles en las reclamaciones . Estas reclamaciones se almacenan en la carga útil del token, que es un código JSON codificado como Base64 . Aquí hay algunos reclamos registrados en el RFC 7519 y lo que significan (lea el RFC completo para más detalles):

  • iss : Director que emitió el token.
  • sub : Principal que es el sujeto del JWT.
  • exp : fecha de caducidad del token.
  • nbf : hora en que el token comenzará a ser aceptado para su procesamiento.
  • iat : Hora en que se emitió el token.
  • jti : Identificador único para el token.

Tenga en cuenta que no debe almacenar datos confidenciales, como contraseñas, en el token.

La carga útil puede ser leída por el cliente y la integridad del token puede verificarse fácilmente al verificar su firma en el servidor. La firma es lo que impide que el token sea manipulado.

No necesitará persistir tokens JWT si no necesita rastrearlos. Sin embargo, si persisten los tokens, tendrá la posibilidad de invalidar y revocar el acceso de los mismos. Para mantener el seguimiento de los tokens JWT, en lugar de persistir el token completo en el servidor, puede persistir el identificador de token (reclamo jti ) junto con otros detalles, como el usuario para el que emitió el token, la fecha de vencimiento, etc.

Cuando persista un token, siempre considere eliminar los antiguos para evitar que su base de datos crezca indefinidamente.

Usando JWT

Hay algunas bibliotecas de Java para emitir y validar tokens JWT tales como:

  • jjwt
  • java-jwt
  • jose4j

Para encontrar otros recursos geniales para trabajar con JWT, eche un vistazo a http://jwt.io .

Manejo de refresco de token con JWT

Acepte solo tokens válidos (y no caducados) para refrescarse. Es responsabilidad del cliente actualizar los tokens antes de la fecha de vencimiento indicada en el reclamo de exp .

Debe evitar que los tokens se actualicen indefinidamente. Vea a continuación algunos enfoques que podría considerar.

Puede mantener el seguimiento del refresco de token agregando dos reclamos a su token (los nombres de los reclamos dependen de usted):

  • refreshLimit : indica cuántas veces se puede actualizar el token.
  • refreshCount : indica cuántas veces se ha actualizado el token.

Así que solo actualice el token si las siguientes condiciones son verdaderas:

  • El token no está vencido ( exp >= now ).
  • El número de veces que se ha actualizado el token es menor que el número de veces que se puede actualizar el token ( refreshCount < refreshLimit ).

Y al actualizar el token:

  • Actualice la fecha de vencimiento ( exp = now + some-amount-of-time ).
  • Incremente la cantidad de veces que el token se ha actualizado ( refreshCount++ ).

De forma alternativa, para mantener el seguimiento de la cantidad de refrescos, puede tener un reclamo que indique la fecha de vencimiento absoluta (que funciona de manera muy similar a la reclamación refreshLimit descrita anteriormente). Antes de la fecha de vencimiento absoluta , cualquier cantidad de refrigerios es aceptable.

Otro enfoque consiste en emitir un token de actualización de larga duración separado que se utiliza para emitir tokens JWT de corta vida.

El mejor enfoque depende de sus requisitos.

Manejo de revocación de token con JWT

Si desea revocar tokens, debe mantener el seguimiento de ellos. No es necesario que almacene el token completo en el servidor, solo almacene el identificador de token (que debe ser exclusivo) y algunos metadatos si lo necesita. Para el identificador de token, puede usar UUID .

La statement jti debe usarse para almacenar el identificador de token en el token. Al validar el token, asegúrese de que no haya sido revocado comprobando el valor de la statement jti contra los identificadores de token que tiene en el lado del servidor.

Por razones de seguridad, revoque todos los tokens para un usuario cuando cambien su contraseña.

Información Adicional

  • No importa qué tipo de autenticación decida usar. Siempre hazlo en la parte superior de una conexión HTTPS para evitar el ataque man-in-the-middle .
  • Eche un vistazo a esta pregunta de Seguridad de la información para obtener más información sobre los tokens.
  • En este artículo , encontrará información útil sobre la autenticación basada en tokens.

Esta respuesta tiene que ver con la autorización y es un complemento de mi respuesta anterior sobre la autenticación

¿Por qué otra respuesta? Intenté expandir mi respuesta anterior agregando detalles sobre cómo admitir las anotaciones JSR-250. Sin embargo, la respuesta original se convirtió en demasiado larga y excedió la longitud máxima de 30,000 caracteres . Así que moví todos los detalles de la autorización a esta respuesta, manteniendo la otra respuesta enfocada en realizar autenticación y emisión de tokens.


Soporte de autorización basada en roles con la anotación @Secured

Además del flujo de autenticación que se muestra en la otra respuesta , la autorización basada en roles puede ser admitida en los puntos finales REST.

Cree una enumeración y defina los roles según sus necesidades:

 public enum Role { ROLE_1, ROLE_2, ROLE_3 } 

Cambie la anotación de enlace de nombre @Secured creada anteriormente para admitir roles:

 @NameBinding @Retention(RUNTIME) @Target({TYPE, METHOD}) public @interface Secured { Role[] value() default {}; } 

Y luego anote las clases de recursos y los métodos con @Secured para realizar la autorización. Las anotaciones de método anularán las anotaciones de clase:

 @Path("/example") @Secured({Role.ROLE_1}) public class ExampleResource { @GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public Response myMethod(@PathParam("id") Long id) { // This method is not annotated with @Secured // But it's declared within a class annotated with @Secured({Role.ROLE_1}) // So it only can be executed by the users who have the ROLE_1 role ... } @DELETE @Path("{id}") @Produces(MediaType.APPLICATION_JSON) @Secured({Role.ROLE_1, Role.ROLE_2}) public Response myOtherMethod(@PathParam("id") Long id) { // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2}) // The method annotation overrides the class annotation // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles ... } } 

Cree un filtro con la prioridad AUTHORIZATION , que se ejecuta después del filtro de prioridad AUTHENTICATION definido anteriormente.

El ResourceInfo se puede usar para obtener el recurso Method y la Class recurso que manejará la solicitud y luego extraer las anotaciones @Secured de ellos:

 @Secured @Provider @Priority(Priorities.AUTHORIZATION) public class AuthorizationFilter implements ContainerRequestFilter { @Context private ResourceInfo resourceInfo; @Override public void filter(ContainerRequestContext requestContext) throws IOException { // Get the resource class which matches with the requested URL // Extract the roles declared by it Class< ?> resourceClass = resourceInfo.getResourceClass(); List classRoles = extractRoles(resourceClass); // Get the resource method which matches with the requested URL // Extract the roles declared by it Method resourceMethod = resourceInfo.getResourceMethod(); List methodRoles = extractRoles(resourceMethod); try { // Check if the user is allowed to execute the method // The method annotations override the class annotations if (methodRoles.isEmpty()) { checkPermissions(classRoles); } else { checkPermissions(methodRoles); } } catch (Exception e) { requestContext.abortWith( Response.status(Response.Status.FORBIDDEN).build()); } } // Extract the roles from the annotated element private List extractRoles(AnnotatedElement annotatedElement) { if (annotatedElement == null) { return new ArrayList(); } else { Secured secured = annotatedElement.getAnnotation(Secured.class); if (secured == null) { return new ArrayList(); } else { Role[] allowedRoles = secured.value(); return Arrays.asList(allowedRoles); } } } private void checkPermissions(List allowedRoles) throws Exception { // Check if the user contains one of the allowed roles // Throw an Exception if the user has not permission to execute the method } } 

Si el usuario no tiene permiso para ejecutar la operación, la solicitud se cancela con un 403 (Prohibido).

Para conocer al usuario que está realizando la solicitud, consulte mi respuesta anterior . Puede obtenerlo desde SecurityContext (que ya debe estar configurado en ContainerRequestContext ) o inyectarlo usando CDI, dependiendo del enfoque que elija.

Si una anotación @Secured no tiene roles declarados, puede suponer que todos los usuarios autenticados pueden acceder a ese punto final, sin tener en cuenta los roles que tienen los usuarios.

Compatibilidad con la autorización basada en roles con anotaciones JSR-250

De forma alternativa a la definición de los roles en la anotación @Secured como se muestra arriba, podría considerar anotaciones JSR-250 como @RolesAllowed , @PermitAll y @DenyAll .

JAX-RS no admite tales anotaciones listas para usar, pero se puede lograr con un filtro. Aquí hay algunas consideraciones a tener en cuenta si desea apoyarlas a todas:

  • @DenyAll en el método tiene prioridad sobre @RolesAllowed y @PermitAll en la clase.
  • @RolesAllowed en el método tiene prioridad sobre @PermitAll en la clase.
  • @PermitAll en el método tiene prioridad sobre @RolesAllowed en la clase.
  • @DenyAll no se puede adjuntar a las clases.
  • @RolesAllowed en la clase tiene prioridad sobre @PermitAll en la clase.

Entonces, un filtro de autorización que verifica las anotaciones de JSR-250 podría ser como:

 @Provider @Priority(Priorities.AUTHORIZATION) public class AuthorizationFilter implements ContainerRequestFilter { @Context private ResourceInfo resourceInfo; @Override public void filter(ContainerRequestContext requestContext) throws IOException { Method method = resourceInfo.getResourceMethod(); // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll if (method.isAnnotationPresent(DenyAll.class)) { refuseRequest(); } // @RolesAllowed on the method takes precedence over @PermitAll RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class); if (rolesAllowed != null) { performAuthorization(rolesAllowed.value(), requestContext); return; } // @PermitAll on the method takes precedence over @RolesAllowed on the class if (method.isAnnotationPresent(PermitAll.class)) { // Do nothing return; } // @DenyAll can't be attached to classes // @RolesAllowed on the class takes precedence over @PermitAll on the class rolesAllowed = resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class); if (rolesAllowed != null) { performAuthorization(rolesAllowed.value(), requestContext); } // @PermitAll on the class if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) { // Do nothing return; } // Authentication is required for non-annotated methods if (!isAuthenticated(requestContext)) { refuseRequest(); } } /** * Perform authorization based on roles. * * @param rolesAllowed * @param requestContext */ private void performAuthorization(String[] rolesAllowed, ContainerRequestContext requestContext) { if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) { refuseRequest(); } for (final String role : rolesAllowed) { if (requestContext.getSecurityContext().isUserInRole(role)) { return; } } refuseRequest(); } /** * Check if the user is authenticated. * * @param requestContext * @return */ private boolean isAuthenticated(final ContainerRequestContext requestContext) { // Return true if the user is authenticated or false otherwise // An implementation could be like: // return requestContext.getSecurityContext().getUserPrincipal() != null; } /** * Refuse the request. */ private void refuseRequest() { throw new AccessDeniedException( "You don't have permissions to perform this action."); } } 

Nota: La implementación anterior se basa en Jersey RolesAllowedDynamicFeature . Si usa Jersey, no necesita escribir su propio filtro, solo use la implementación existente.