1. Información general
Este artículo continúa la serie Registro con Spring Security con un vistazo a cómo implementar correctamente los roles y privilegios .
2. Privilegio y función del usuario
Primero, comencemos con nuestras entidades. Tenemos tres entidades principales:
- el usuario
- el rol : representa los roles de alto nivel del usuario en el sistema; cada rol tendrá un conjunto de privilegios de bajo nivel
- el privilegio : representa un privilegio / autoridad granular de bajo nivel en el sistema
Aquí está el usuario :
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstName; private String lastName; private String email; private String password; private boolean enabled; private boolean tokenExpired; @ManyToMany @JoinTable( name = "users_roles", joinColumns = @JoinColumn( name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn( name = "role_id", referencedColumnName = "id")) private Collection roles; }
Como puede ver, el usuario contiene los roles, pero también algunos detalles adicionales que son necesarios para un mecanismo de registro adecuado.
A continuación, aquí está el papel :
@Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @ManyToMany(mappedBy = "roles") private Collection users; @ManyToMany @JoinTable( name = "roles_privileges", joinColumns = @JoinColumn( name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn( name = "privilege_id", referencedColumnName = "id")) private Collection privileges; }
Y finalmente el privilegio :
@Entity public class Privilege { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @ManyToMany(mappedBy = "privileges") private Collection roles; }
Como puede ver, estamos considerando tanto el rol de usuario como las relaciones de privilegio de rol bidireccional de varios a varios .
3. Configurar privilegios y roles
A continuación, centrémonos en realizar una configuración inicial de los privilegios y roles en el sistema.
Vincularemos esto al inicio de la aplicación y usaremos un ApplicationListener en ContextRefreshedEvent para cargar nuestros datos iniciales en el inicio del servidor:
@Component public class SetupDataLoader implements ApplicationListener { boolean alreadySetup = false; @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @Autowired private PrivilegeRepository privilegeRepository; @Autowired private PasswordEncoder passwordEncoder; @Override @Transactional public void onApplicationEvent(ContextRefreshedEvent event) { if (alreadySetup) return; Privilege readPrivilege = createPrivilegeIfNotFound("READ_PRIVILEGE"); Privilege writePrivilege = createPrivilegeIfNotFound("WRITE_PRIVILEGE"); List adminPrivileges = Arrays.asList( readPrivilege, writePrivilege); createRoleIfNotFound("ROLE_ADMIN", adminPrivileges); createRoleIfNotFound("ROLE_USER", Arrays.asList(readPrivilege)); Role adminRole = roleRepository.findByName("ROLE_ADMIN"); User user = new User(); user.setFirstName("Test"); user.setLastName("Test"); user.setPassword(passwordEncoder.encode("test")); user.setEmail("[email protected]"); user.setRoles(Arrays.asList(adminRole)); user.setEnabled(true); userRepository.save(user); alreadySetup = true; } @Transactional Privilege createPrivilegeIfNotFound(String name) { Privilege privilege = privilegeRepository.findByName(name); if (privilege == null) { privilege = new Privilege(name); privilegeRepository.save(privilege); } return privilege; } @Transactional Role createRoleIfNotFound( String name, Collection privileges) { Role role = roleRepository.findByName(name); if (role == null) { role = new Role(name); role.setPrivileges(privileges); roleRepository.save(role); } return role; } }
Entonces, ¿qué sucede durante este sencillo código de configuración? Nada complicado:
- estamos creando los privilegios
- estamos creando los roles y asignándoles los privilegios
- estamos creando un usuario y asignándole un rol
Tenga en cuenta cómo estamos usando un indicador ya Configuración para determinar si la configuración debe ejecutarse o no . Esto se debe simplemente a que, dependiendo de la cantidad de contextos que haya configurado en su aplicación, el ContextRefreshedEvent puede activarse varias veces. Y solo queremos que la configuración se ejecute una vez.
Dos notas rápidas aquí: primero, sobre terminología . Estamos usando los términos Privilegio - Rol aquí, pero en Spring, estos son ligeramente diferentes. En Spring, nuestro Privilegio se conoce como Rol y también como una autoridad (otorgada), lo cual es un poco confuso. No es un problema para la implementación, por supuesto, pero definitivamente vale la pena señalarlo.
Segundo: estos Spring Roles (nuestros Privilegios) necesitan un prefijo ; de forma predeterminada, ese prefijo es "ROLE", pero se puede cambiar. No usamos ese prefijo aquí, solo para simplificar las cosas, pero tenga en cuenta que si no lo está cambiando explícitamente, será necesario.
4. UserDetailsService personalizado
Ahora, veamos el proceso de autenticación.
Veremos cómo recuperar al usuario dentro de nuestro UserDetailsService personalizado , y cómo mapear el conjunto correcto de autoridades a partir de los roles y privilegios que el usuario ha asignado:
@Service("userDetailsService") @Transactional public class MyUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Autowired private IUserService service; @Autowired private MessageSource messages; @Autowired private RoleRepository roleRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if (user == null) { return new org.springframework.security.core.userdetails.User( " ", " ", true, true, true, true, getAuthorities(Arrays.asList( roleRepository.findByName("ROLE_USER")))); } return new org.springframework.security.core.userdetails.User( user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, getAuthorities(user.getRoles())); } private Collection getAuthorities( Collection roles) { return getGrantedAuthorities(getPrivileges(roles)); } private List getPrivileges(Collection roles) { List privileges = new ArrayList(); List collection = new ArrayList(); for (Role role : roles) { collection.addAll(role.getPrivileges()); } for (Privilege item : collection) { privileges.add(item.getName()); } return privileges; } private List getGrantedAuthorities(List privileges) { List authorities = new ArrayList(); for (String privilege : privileges) { authorities.add(new SimpleGrantedAuthority(privilege)); } return authorities; } }
Lo interesante a seguir aquí es cómo se asignan los privilegios (y roles) a las entidades GrantedAuthority.
Este mapeo hace que toda la configuración de seguridad sea altamente flexible y poderosa : puede mezclar y combinar roles y privilegios tan granulares como sea necesario y, al final, se asignarán correctamente a las autoridades y se devolverán al marco.
5. Registro de usuario
Finalmente, echemos un vistazo al registro de un nuevo usuario.
Hemos visto cómo la configuración crea el usuario y le asigna roles (y privilegios); ahora echemos un vistazo a cómo se debe hacer esto durante el registro de un nuevo usuario:
@Override public User registerNewUserAccount(UserDto accountDto) throws EmailExistsException { if (emailExist(accountDto.getEmail())) { throw new EmailExistsException ("There is an account with that email adress: " + accountDto.getEmail()); } User user = new User(); user.setFirstName(accountDto.getFirstName()); user.setLastName(accountDto.getLastName()); user.setPassword(passwordEncoder.encode(accountDto.getPassword())); user.setEmail(accountDto.getEmail()); user.setRoles(Arrays.asList(roleRepository.findByName("ROLE_USER"))); return repository.save(user); }
En esta implementación simple, asumimos que se está registrando un usuario estándar, por lo que se le asigna el rol ROLE_USER .
Por supuesto, la lógica más compleja se puede implementar fácilmente de la misma manera, ya sea con varios métodos de registro codificados o permitiendo que el cliente envíe el tipo de usuario que se está registrando.
6. Conclusión
En este tutorial, ilustramos cómo implementar Roles y Privilegios con JPA, para un sistema respaldado por Spring Security.
La implementación completa de este tutorial de Registro con Spring Security se puede encontrar en el proyecto GitHub; este es un proyecto basado en Maven, por lo que debería ser fácil de importar y ejecutar tal como está.