Inserts:
insert into user_role (username, role, granted_date)
values ('admin','ADMIN',NOW());
insert into user_role (username, role, granted_date)
values ('customer','CUSTOMER',NOW());
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 implementaequalsyhashCode.
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:
admin con rol admin.customer con rol customer.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::getRolees 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.
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.
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 implementaequalsyhashCode.
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.
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.
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:
admin con rol admin.customer con rol customer.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.
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::getRolees más limpio que una lambda explícita y deja claro que solo extraes una propiedad de cada objeto.
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.
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.
Inserts:
insert into user_role (username, role, granted_date)
values ('admin','ADMIN',NOW());
insert into user_role (username, role, granted_date)
values ('customer','CUSTOMER',NOW());
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
import java.util.Objects;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserRoleId implements Serializable {
private String username;
private String role;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof UserRoleId that)) return false;
return Objects.equals(username, that.username) && Objects.equals(role, that.role);
}
@Override
public int hashCode() {
return Objects.hash(username, role);
}
}
package com.platzi.pizza.persistence.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_role")
@IdClass(UserRoleId.class)
@Getter
@Setter
@NoArgsConstructor
public class UserRoleEntity {
@Id
@Column(nullable = false, length = 20)
private String username;
@Id
@Column(nullable = false, length = 20)
private String role;
@Column(name = "granted_date", nullable = false, columnDefinition = "DATETIME")
private LocalDateTime grantedDate;
@ManyToOne
@JoinColumn(name = "username", referencedColumnName = "username", insertable = false, updatable = false)
private UserEntity user;
}
No entiendo porque se usa la anotacion @IdClass(UserRoleId.class) y no un @EmbedId, cuando se emplea uno y otro?
Por lo que entiendo a lo largo del curso los roles pueden tener cualquier descripción en vez de ADMIN, Administrador sin embargo estas deben de coincidir con el requestMatchers?
Eso es algo que me cuesta entender muchas veces con el uso de ORM, en lo personal el modelado usando JPA , mi formación de BD me crea ciertos issues con los conceptos de normalizar tablas de base de datos usando ORM. Si seguimos los principios de normalización hasta lo más atómicos debería ser algo como :
Customer ---< customer_roles >--- Rol
Un customer puede tener uno o más roles asociados.
Un rol puede tener asociado uno o más usuarios.
Dos entidades con relaciones de muchos a muchos que da como resultado la tabla (asociativa) customer_roles, en la cual además de tener ambas llaves primarias, atributos como granted_date debería de ir ahí. Quedando de la siguiente manera:
| customer_roles |
|-----------------|
| customer_id |
| rol_id |
| granted_date |
Los ORM se comen las tablas intermedias.
Con el esquema actual el valor actual rol "Admin|customer" se repite por cada usuario además de setear el nombre de usuario, duplicando valores en diferentes tablas.
Esa seria mi única observación de lo que no me gusta en cuanto a ORMs.
una pregunta porque tenemos que hacer otra tabla como user_role porque no simplemente a la tabla users se le agrega nomas el role?
No entiendo muy bien para que sirven los metodos
equals y hashcode de la tabla UserRoleId :cr
UserRoleEntity
package com.platzi.pizzeria.persistence.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_role")
@IdClass(UserRoleId.class)
@Getter
@Setter
@NoArgsConstructor
public class UserRoleEntity {
@Id
@Column(nullable = false, length = 20)
private String username;
@Id
@Column(nullable = false, length = 20)
private String role;
@Column(name = "granted_date", nullable = false, columnDefinition = "DATETIME")
private LocalDateTime grantedDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "username", referencedColumnName = "username", insertable = false, updatable = false)
private UserEntity user;
}
U
package com.platzi.pizzeria.persistence.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Entity
@Table(name = "user")
@Getter
@Setter
@NoArgsConstructor
public class UserEntity {
@Id
@Column(nullable = false, length = 20)
private String username;
@Column(nullable = false, length = 200)
private String password;
@Column(length = 50)
private String email;
@Column(nullable = false, columnDefinition = "TINYINT")
private Boolean locked;
@Column(nullable = false, columnDefinition = "TINYINT")
private Boolean disabled;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<UserRoleEntity> roles;
}
UserRoleId
package com.platzi.pizzeria.persistence.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
import java.util.Objects;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserRoleId implements Serializable {
private String username;
private String role;
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(!(o instanceof UserRoleId that)) return false;
return Objects.equals(username, that.username) && Objects.equals(role, that.role);
}
@Override
public int hashCode() {
return Objects.hash(username, role);
}
}
UserSecuriryService
package com.platzi.pizzeria.service;
import com.platzi.pizzeria.persistence.entity.UserEntity;
import com.platzi.pizzeria.persistence.entity.UserRoleEntity;
import com.platzi.pizzeria.persistence.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserSecurityService implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserSecurityService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = this.userRepository.findById(username)
.orElseThrow(() -> new UsernameNotFoundException("User + " + username + " NOT FOUND"));
String[] roles = userEntity.getRoles().stream().map(UserRoleEntity::getRole).toArray(String[]::new);
return User.builder()
.username(userEntity.getUsername())
.password(userEntity.getPassword())
.roles(roles)
.accountLocked(userEntity.getLocked())
.disabled(userEntity.getDisabled())
.build();
}
}
Otra forma de convertir un objeto del tipo List<T> a un objeto del tipo T[]:
String[] roles = userEntity.getRoles().toArray(new String[userEntity.getRoles().size()]);
```String\[] roles = userEntity.getRoles().toArray(new String\[userEntity.getRoles().size()]);