In our last article, we talked about Granted Authority vs Role. In this article, we will look at the Spring security roles and privileges and how to use this feature to build your application.
Spring Security Roles and Permissions
There are multiple parts of an enterprise application, and it allows not all users to access the entire application. We may come up with requirement where we like to provide access to the application based on the user roles and privileges. Let’s take example of a simple back-end application managing the commerce store.
- The user with
ADMIN
role will have full permission to perform any action. - A customer service agent can read customer and order information but doesn’t see other options.
- A product manager can only see options to update/ create products.
Spring security makes it more easy to build these types of rules using the roles and privileges. We can assign roles and privileges to the user during registration/ creation and these roles.In this article, we will see how to use the Spring security roles and privileges feature to handle such use cases. To make sure we have common understanding, let’s look at few important terms.
- Roles – Role represents high-level role in system (e.g.
ADMIN
,MANAGER
etc.), Each role can have low-level privileges. - Privileges – The privileges define the low level authority for a Role (e.g.
ADMIN
can read/write/delete butMANAGER
can only read/edit)
This article is part of our Spring Security course, you can download the complete application from our GitHub Repository.
1. Database Design
There are multiple way to design the spring security roles and permissions but one of the most common and flexible way is to build and roles and privileges module around user groups. As part of any application, put the users in some groups, let’s take the following example for better understanding:
- A frontend user should go to
CUSTOMER
Group. - Back-end users can go to
EMPLOYEE
group. - We can create another variation of backed user (e.g.
ADMIN
,MANAGER
etc.)
We will use the same concept of our application. Each user of the application will be part of a certain group, and we will use these groups to drive the roles and permissions. Here is the database design for our application.
- Each user belongs to a certain group.
- Groups will be assigned to the user at registration/ creation time.
principle_group
defines all the groups available in the system (e.g. customer, admin etc.)
I will not cover the user entity, as we already cover this in the Registration with Spring Security and Spring Boot article.
1.1. UserGroup Entity
Here is our entity class for the UserGroup
@Entity
@Table(name = "principle_groups")
public class Group{
//removed getter and setter to save space
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String code;
private String name;
@ManyToMany(mappedBy = "userGroups")
private Set<UserEntity> users;
}
The Group
is a simple JPA entity and contains information about the group name and code. The interesting part is the @ManyToMany
relation with User
entity. This many-to-many relations will create another database table for us (see above DB table diagram).
1.2. User Entity Class
Here is our UserEntity
class. The highlighting point is the relation with the Group
Entity.
@Entity
@Table(name = "user")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
@Column(unique = true)
private String email;
private String password;
private String token;
private boolean accountVerified;
private int failedLoginAttempts;
private boolean loginDisabled;
@OneToMany(mappedBy = "user")
private Set<SecureToken> tokens;
@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "user_groups",
joinColumns =@JoinColumn(name = "customer_id"),
inverseJoinColumns = @JoinColumn(name = "group_id"
))
private Set<Group> userGroups= new HashSet<>();
public Set<Group> getUserGroups() {
return userGroups;
}
public void setUserGroups(Set<Group> userGroups) {
this.userGroups = userGroups;
}
}
We can fill the data in principle_groups table based on your requirement. We are filling the following 2 groups:
- Customer
- Admin
2. Adding Groups to Users
We base our entire logic to derive the spring security roles and permissions on the assigned group to the user. Let’s change the registration process to assign the user group to the user. We will add a minor change to our DefaultUserService
. During the registration process, we will add the group to the user profile.
@Service("userService")
public class DefaultUserService implements UserService{
@Autowired
private UserRepository userRepository;
@Autowired
UserGroupRepository groupRepository;
@Override
public void register(UserData user) throws UserAlreadyExistException {
if(checkIfUserExist(user.getEmail())){
throw new UserAlreadyExistException("User already exists for this email");
}
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(user, userEntity);
encodePassword(user, userEntity);
updateCustomerGroup(userEntity);
userRepository.save(userEntity);
sendRegistrationConfirmationEmail(userEntity);
}
private void updateCustomerGroup(UserEntity userEntity){
Group group= groupRepository.findByCode("customer");
userEntity.addUserGroups(group);
}
}
You can always change the group assignment logic based on your requirement. We can even build logic in our back-end system to assign groups to the user.
3. Custom UserDetailsService
I have already explained the importance of UserDetailsService
class in Spring security. The UserDetailsService is a core interface in Spring Security framework, which is used to retrieve the user’s authentication and authorization information. This interface is also responsible to provide the User’s GrantedAuthority
list, which is used to derive our spring security roles and permissions for the user. Let’s change spring security custom UserDetailsService to return list of GrantedAuthority
based on user groups.
@Service("userDetailsService")
@Transactional
public class CustomUserDetailService implements UserDetailsService{
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
final UserEntity customer = userRepository.findByEmail(email);
if (customer == null) {
throw new UsernameNotFoundException(email);
}
boolean enabled = !customer.isAccountVerified(); // we can use this in case we want to activate account after customer verified the account
UserDetails user = User.withUsername(customer.getEmail())
.password(customer.getPassword())
.disabled(customer.isLoginDisabled())
.authorities(getAuthorities(customer)).build()
;
return user;
}
private Collection<GrantedAuthority> getAuthorities(UserEntity user){
Set<Group> userGroups = user.getUserGroups();
Collection<GrantedAuthority> authorities = new ArrayList<>(userGroups.size());
for(Group userGroup : userGroups){
authorities.add(new SimpleGrantedAuthority(userGroup.getCode().toUpperCase()));
}
return authorities;
}
}
The interesting thing to follow here is how we are building the GrantedAuthority
entities.We are using a simple logic to build the GrantedAuthority
list same as the assigned user groups. You can change / customize the logic to build more complex GrantedAuthorit
ies.
4. Spring Security Authority Mapping
With this change in the UserDetailsService, we can start using the authorities to handle the visibility on the UI using hasAnyAuthority()
or hasAuthority()
method. Let’s look at the modified spring security configuration.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login", "/register","/home")
.permitAll()
.antMatchers("/account/**").hasAnyAuthority("CUSTOMER", "ADMIN")
.and()
...
}
Looking at the above configuration, we are telling Spring security to only allow a user with CUSTOMER
and ADMIN
authority to access the /account/**
pattern.Remember, the user authorities are provided by the UserDetailsService
. You can also use the same option to show/ hide the links based on the user roles. Here is a sample code using Spring security with Thymeleaf.
<ul class="navbar-nav ml-auto">
<li class="dropdown user user-menu" sec:authorize="hasAnyAuthority('CUSTOMER', 'ADMIN')">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="hidden-xs" sec:authentication="name"></span>
</a>
<ul class="dropdown-menu">
<li class="user-header">
<img th:src="@{/dist/img/avatar5.png}" class="img-circle" alt="User Image">
<p>
Spring Security Course
<small>Java Development Journal</small>
</p>
</li>
<li class="user-footer">
<div class="pull-right">
<a href="javascript: document.logoutForm.submit()" class="btn btn-default btn-flat">Sign out</a>
</div>
</li>
</ul>
</li>
<form name="logoutForm" th:action="@{/logout}" method="post" th:hidden="true">
<input hidden type="submit" value="Sign Out"/>
</form>
</ul>
The interesting part is sec:authorize="hasAnyAuthority('CUSTOMER', 'ADMIN')
, which shows that only user with authority of CUSTOMER
and ADMIN
can access this section. For other user, this will not be available.
We will cover more Granular level authorization (e.g. read/ write etc.) in our next article.
Summary
In this section we learned the spring security roles and permissions workflow and architecture. We learned how to build the roles and permissions around the user groups and how to use the custom UserDetailsService
to build the GrantedAuthority
list. The source code for this article will be available on our GitHub repository.