Saltar al contenido

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

Indagamos por internet y así brindarte la solución a tu duda, en caso de dificultades deja tu comentario y contestamos porque estamos para ayudarte.

Solución:

Cómo funciona la autenticación basada en tokens

En la autenticación basada en token, el cliente intercambia credenciales duras (como nombre de usuario y contraseña) para un dato llamado simbólico. Para cada solicitud, en lugar de enviar las credenciales físicas, 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 previamente 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 realizada 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 apátrida autenticación).

Qué puede 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 implementaciones 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 que ofrece el contenedor de servlets y que se puede configurar a través de la aplicación web.xml descriptor. 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 recursos 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 lanza alguna excepción al validar las credenciales, una respuesta con el estado 403 (Prohibido) será devuelto.

Si las credenciales se validan con éxito, una respuesta con el estado 200 Se devolverá (OK) y el token emitido se enviará al cliente en la carga útil 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 útil de la solicitud:

username=admin&password=123456

En lugar de parámetros de formulario, es posible envolver 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

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


  "username": "admin",
  "password": "123456"

Extraer el token de la solicitud y validarlo

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

Authorization: Bearer 

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

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

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

La anotación de enlace de nombre definida anteriormente se utilizará 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. los 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, una respuesta con el estado 401 (No autorizado) será devuelto. De lo contrario, la solicitud procederá a un método de recurso.

Asegurar sus puntos finales REST

Para vincular el filtro de autenticación a métodos de recursos o clases de recursos, anótelos con el @Secured anotación creada arriba. Para los métodos y / o clases que se anotan, se ejecutará el filtro. Significa que tales puntos finales solamente se alcanzará 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á solamente Para el mySecuredMethod(Long) método porque está anotado con @Secured.

Identificación del usuario actual

Es muy probable que necesite conocer al usuario que está realizando la solicitud contra su API REST. Se pueden utilizar los siguientes enfoques para lograrlo:

Anulando el contexto de seguridad de la solicitud actual

Dentro de tu ContainerRequestFilter.filter(ContainerRequestContext) método, un nuevo SecurityContext La instancia se puede configurar para la solicitud actual. Luego anule el SecurityContext.getUserPrincipal(), devolviendo un Principal ejemplo:

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

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

Inyectar el 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 obtén el Principal:

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

Uso de CDI (inyección de contexto y dependencia)

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

Cree un calificador CDI:

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

En tus AuthenticationFilter creado arriba, inyecte 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, busque un User instancia con el nombre de usuario correspondiente y asígnelo al authenticatedUser campo productor:

@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
    

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

@Inject
@AuthenticatedUser
User authenticatedUser;

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

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

Asegúrese de usar el CDI @Produces anotación en tu AuthenticatedUserProducer frijol.

La clave aquí es el frijol anotado con @RequestScoped, lo 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 alcance de solicitud y luego leerlo de sus clases de recursos JAX-RS.

En comparación con el enfoque que anula el SecurityContext, el enfoque CDI le permite obtener el usuario autenticado de beans distintos de los recursos y proveedores JAX-RS.

Apoyo a la autorización basada en roles

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

Emitir tokens

Un token puede ser:

  • Opaco: No revela más detalles que 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. Aquí se puede ver un buen ejemplo de cómo generar una cadena aleatoria en Java. También puedes usar:

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

JWT (token web JSON)

JWT (JSON Web Token) es un método estándar para representar 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 reclamación (es. Estas afirmaciones se almacenan en la carga útil del token, que es un JSON codificado como Base64. Aquí hay algunas afirmaciones registradas en el RFC 7519 y lo que significan (lea el RFC completo para obtener más detalles):

  • iss: Principal que emitió el token.
  • sub: Principal que es el tema del JWT.
  • exp: Fecha de vencimiento del token.
  • nbf: Hora en la que el token comenzará a ser aceptado para su procesamiento.
  • iat: Hora a la que se emitió el token.
  • jti: Identificador único del token.

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

El cliente puede leer la carga útil y la integridad del token puede comprobarse fácilmente verificando su firma en el servidor. La firma es lo que evita que se manipule el token.

No necesitará conservar los tokens JWT si no necesita rastrearlos. No obstante, al persistir los tokens, tendrás la posibilidad de invalidar y revocar el acceso a los mismos. Para realizar un seguimiento de los tokens JWT, en lugar de conservar el token completo en el servidor, puede conservar el identificador del token (jti reclamo) junto con algunos otros detalles, como el usuario para el que emitió el token, la fecha de vencimiento, etc.

Al conservar los tokens, 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 como:

  • jjwt
  • java-jwt
  • jose4j

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

Manejo de la revocación de tokens con JWT

Si desea revocar tokens, debe realizar un seguimiento de ellos. No necesita almacenar el token completo en el lado del servidor, almacene solo el identificador del token (que debe ser único) y algunos metadatos si lo necesita. Para el identificador de token, puede usar UUID.

los jti reclamo debe usarse para almacenar el identificador de token en el simbólico. Al validar el token, asegúrese de que no haya sido revocado comprobando el valor del jti reclamar contra los identificadores de token que tiene en el lado del servidor.

Por motivos de seguridad, revoque todos los tokens de un usuario cuando cambie su contraseña.

Información adicional

  • No importa qué tipo de autenticación decida utilizar. Siempre hágalo 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 token.

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

Por qué otro ¿respuesta? Intenté expandir mi respuesta anterior agregando detalles sobre cómo admitir anotaciones JSR-250. Sin embargo, la respuesta original se convirtió en el camino demasiado largo y superó 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 la autenticación y emitir tokens.


Apoyar la autorización basada en roles con el @Secured anotación

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

Crea una enumeración y define los roles según tus necesidades:

public enum Role 
    ROLE_1,
    ROLE_2,
    ROLE_3

Cambiar el @Secured anotación de enlace de nombre creada antes para admitir roles:

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

Y luego anote las clases de recursos y métodos con @Secured para realizar la autorización. Las anotaciones del método anularán las anotaciones de la 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 el AUTHORIZATION prioridad, que se ejecuta después de la AUTHENTICATION filtro de prioridad definido previamente.

los ResourceInfo se puede utilizar para obtener el recurso Method y recurso Class que manejará la solicitud y luego extraerá el @Secured anotaciones 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 del SecurityContext (que ya debería estar configurado en el ContainerRequestContext) o inyéctelo usando CDI, dependiendo del enfoque que elija.

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

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

Alternativamente a definir los roles en el @Secured anotación como se muestra arriba, podría considerar anotaciones JSR-250 como @RolesAllowed, @PermitAll y @DenyAll.

JAX-RS no admite este tipo de anotaciones listas para usar, pero se podría lograr con un filtro. Aquí hay algunas consideraciones que debe tener en cuenta si desea admitirlas 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 clases.
  • @RolesAllowed en la clase tiene prioridad sobre @PermitAll en la clase.

Entonces, un filtro de autorización que verifica las anotaciones 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.

Sección de Reseñas y Valoraciones

Si piensas que ha resultado de ayuda este post, te agradeceríamos que lo compartas con otros programadores de esta manera nos ayudas a extender nuestro contenido.

¡Haz clic para puntuar esta entrada!
(Votos: 2 Promedio: 5)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *