I usually post about Security on Google+ - you can follow me there:
1. Overview
This article continues the Registration with Spring Security series with a look at how to properly implement Roles and Privileges.
2. User Role and Privilege
First, let’s start with our entities. We have three main entities:
- the User
- the Role – this represents the high level roles of the user in the system; each role will have a set of low level privileges
- the Privilege – represents a low level, granular privilege/authority in the system
Here’s the user:
@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<Role> roles; }
As you can see, the user contains the roles, but also a few additional details that are necessary for a proper registration mechanism.
Next – here’s the role:
@Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @ManyToMany(mappedBy = "roles") private Collection<User> users; @ManyToMany @JoinTable( name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id")) private Collection<Privilege> privileges; }
And finally the privilege:
@Entity public class Privilege { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @ManyToMany(mappedBy = "privileges") private Collection<Role> roles; }
As you can see, we’re considering both the User <-> Role as well as the Role <-> Privilege relationships many-to-many bidirectional.
3. Setup Privileges and Roles
Next – let’s focus on doing some early setup of the Privileges and Roles in the system.
We’ll tie this to the startup of the application and we’ll use an ApplicationListener on ContextRefreshedEvent to load our initial data on server start:
@Component public class InitialDataLoader implements ApplicationListener<ContextRefreshedEvent> { 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<Privilege> 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("test@test.com"); user.setRoles(Arrays.asList(adminRole)); user.setEnabled(true); userRepository.save(user); alreadySetup = true; } @Transactional private Privilege createPrivilegeIfNotFound(String name) { Privilege privilege = privilegeRepository.findByName(name); if (privilege == null) { privilege = new Privilege(name); privilegeRepository.save(privilege); } return privilege; } @Transactional private Role createRoleIfNotFound(String name, Collection<Privilege> privileges) { Role role = roleRepository.findByName(name); if (role == null) { role = new Role(name); role.setPrivileges(privileges); roleRepository.save(role); } return role; } }
So, what’s happening during this simple setup code? Nothing complicated:
- we’re creating the privileges
- we’re creating the roles and assigning the privileges to them
- we’re creating a user and assigning a role to it
Note how we’re using an alreadySetup flag to determine if the setup needs to run or not. This is simply because, depending on how many context you have configured in your application – the ContextRefreshedEvent may be fired multiple times. And we only want setup to be executed once.
4. Custom UserDetailsService
Now – let’s check out the authentication process.
We’re going to see how to retrieve the user within our custom UserDetailsService, and how to map the right set of authorities from the roles and privileges the user has assigned:
@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<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) { return getGrantedAuthorities(getPrivileges(roles)); } private List<String> getPrivileges(Collection<Role> roles) { List<String> privileges = new ArrayList<String>(); List<Privilege> collection = new ArrayList<Privilege>(); for (Role role : roles) { collection.addAll(role.getPrivileges()); } for (Privilege item : collection) { privileges.add(item.getName()); } return privileges; } private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) { List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); for (String privilege : privileges) { authorities.add(new SimpleGrantedAuthority(privilege)); } return authorities; } }
The interesting thing to follow here is how the Privileges (and Roles) are mapped to GrantedAuthority entities.
This mapping makes the entire security configuration highly flexible and powerful – you can mix and match roles and privileges as granular as necessary, and at the end, they’ll be correctly mapped to authorities and returned back to the framework.
5. User Registration
Finally – let’s take a look at registration for a new user.
We’ve seen how setup goes about creating the User and assigns Roles (and Privileges) to it – let’s now take a look at how this needs to be done during registration of a new user:
@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); }
In this simple implementation, we’re assuming that a standard user is being registered, so the ROLE_USER role is assigned to it.
Of course more complex logic can easily be implemented in the same way – either by having multiple, hardcoded registration methods, or by allowing the client to send the type of user that’s being registered.
6. Conclusion
In this tutorial, we illustrated how to implement Roles and Privileges with JPA, for a Spring Security backed system.
The full implementation of this Registration with Spring Security tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.