Roles por usuario en Spring Security con JPA

Resumen

Asignar roles a usuarios en Spring Security te permite controlar qué endpoints puede consumir cada persona en tu API. Aquí aprenderás a crear una tabla de roles relacionada con tu tabla de usuarios, mapearla con JPA y conectarla a tu implementación de UserDetailsService para que los permisos dejen de ser uniformes y se segmenten correctamente.

Por qué necesitas una tabla de roles separada

Cuando autenticas usuarios desde la base de datos, asignar el mismo rol a todos rompe el control de acceso. La solución pasa por modelar los roles en una tabla independiente y traerlos junto con cada usuario.

La tabla user_role se relaciona con user y contiene tres atributos: username, role y granted_date. Los dos primeros forman una clave primaria compuesta, y el tercero registra desde cuándo ese usuario tiene ese permiso. Puedes generarla con Hibernate o con el script SQL de los recursos.

¿Qué es una clave primaria compuesta en JPA? Es una clave formada por dos o más columnas. En JPA la representas con una clase serializable anotada como @IdClass, que implementa equals y hashCode.

Cómo se mapea la entidad UserRoleEntity en JPA

Dentro del paquete entity, la clase UserRoleEntity declara username y role como parte de la clave primaria, y agrega grantedDate para saber desde cuándo aplica el permiso [01:30].

La anotación @IdClass apunta a UserRoleID, una clase auxiliar que contiene username y role, implementa Serializable y sobrescribe equals y hashCode. Además, la entidad mantiene una relación con el usuario, ya que cada permiso pertenece a un usuario puntual.

Cómo declarar la relación inversa en UserEntity

En UserEntity falta la contraparte. Crea una lista del tipo List<UserRoleEntity> llamada roles y anótala con @OneToMany, indicando mappedBy hacia el atributo user de UserRoleEntity. Esto cierra la relación bidireccional.

Usa FetchType.EAGER para que los roles se consulten automáticamente cada vez que recuperas un UserEntity. Así evitas hacer queries extra y siempre tienes los permisos disponibles cuando construyes el usuario de seguridad.

Cómo asignar roles existentes desde MySQL Workbench

Después de lanzar la aplicación, Hibernate crea la tabla en la base de datos. Refrescas en MySQL Workbench y verificas con un SELECT que la tabla existe pero está vacía.

Para replicar los roles que tenías en autenticación en memoria, inserta dos registros:

  • Usuario admin con rol admin.
  • Usuario customer con rol customer.
  • Para granted_date escribes now() sin comillas para que MySQL asigne la fecha actual al aplicar.

Un mismo usuario puede tener varios roles porque la clave primaria compuesta lo permite. Esa flexibilidad es clave cuando un perfil necesita combinar permisos.

Cómo transformar la lista de roles en el UserDetailsService

En UserSecurityService, el método que construye el UserDetails espera un arreglo de strings con los roles, pero UserEntity devuelve una lista de UserRoleEntity. Toca convertir.

El flujo queda así después de recuperar el usuario de la base de datos:

java String[] roles = userEntity.getRoles() .stream() .map(UserRoleEntity::getRole) .toArray(String[]::new);

Con stream recorres la lista, con map extraes el campo role que sí es string, y con toArray(String[]::new) obtienes el arreglo que necesita Spring Security. Ese arreglo se lo pasas al constructor del usuario que retornas [04:50].

¿Por qué usar method reference en el map? Porque UserRoleEntity::getRole es más limpio que una lambda explícita y deja claro que solo extraes una propiedad de cada objeto.

Cómo verificar la segmentación de permisos en Postman

Reinicia la aplicación y prueba el endpoint /orders con Postman. Con admin y admin123 la petición responde 200, porque la configuración de seguridad permite ese rol en ese recurso.

Si intentas con el usuario customer, recibes un 403. La autenticación funciona, pero el filter chain no le otorga acceso a /orders. Así confirmas que los roles ya se leen desde la base de datos y no desde memoria.

Qué pasa cuando bloqueas una cuenta desde MySQL

Si cambias la propiedad locked del usuario admin a 1 y vuelves a lanzar la petición, recibes un 401. En el log aparece que no se encontró el usuario, porque la cuenta está bloqueada aunque la contraseña sea correcta.

Al regresar locked a 0, la cuenta se reactiva y la petición vuelve a responder 200. Este comportamiento depende de cómo Spring Security interpreta los flags de la cuenta dentro del UserDetails.

Con esto los permisos quedan segmentados por roles y tu API responde solo a quienes tienen acceso explícito. ¿Has tenido conflictos con FetchType.EAGER en relaciones grandes? Cuéntame cómo lo resolviste.