02 Build A Role Based Access Control in A Spring Boot 3 API
02 Build A Role Based Access Control in A Spring Boot 3 API
Implement Role-based
Control in Spring Boot 3Access
Eric Cabrel TIOGO
Oct 15, 2023 • 10 min read
What we will do
We have a Web API that has public routes and restricted routes. The
restricted routes require a valid JWT from a user in the database.
Now that the user is authenticated, we want to go further by allowing
access to some data only if the user has a specific role.
Here are the following roles we have in our system:
User: can access his information
Administrator: can do everything the User role does and
access the users' list.
Super Administrator: can do everything the Admin role does
and create an admin user; shortly, he can do everything.
The table below lists the protected routes with the role required to
access them.
API ROUTE ROLE REQUIRED DESCRIPTION
[GET] /users/me User, Admin, Super Admin Retrieve the authenticated user
[GET] /users Admin, Super Admin Retrieve the list of all users
Prerequisites
To follow this tutorial, make sure you have the following tools installed
on your computer.
JDK 11 or higher - Download link
Maven 3.8 or higher - Download link
Docker - Download link
Has followed this tutorial about JWT authentication in Spring
Boot.
We need Docker to run a container for MySQL 8; you can skip it if
MySQL is installed on your computer. Run the command below to start
the Docker container from the MySQL image:
docker run -d -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=taskdb --name
cd blog-tutorials
git checkout @
cd springboot-jwt-auth
mvn spring-boot:run
Before running the last command, ensure the MySQL Docker container
is running. The application will start on port 8005.
The Spring Boot application runs on port 8005.
Let's authenticate with the user created previously using the following
cURL request.
package com.tericcabrel.authapi.entities;
package com.tericcabrel.authapi.entities;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.util.Date;
@Table(name = "roles")
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Integer id;
@Column(unique = true, nullable = false)
@Enumerated(EnumType.STRING)
private RoleEnum name;
@Column(nullable = false)
private String description;
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private Date createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Date updatedAt;
// Getters and setters here....
}
Create the "RoleRepository.java" which represents the Data Access
Layer for the Role entity.
package com.tericcabrel.authapi.repositories;
import com.tericcabrel.authapi.entities.Role;
import com.tericcabrel.authapi.entities.RoleEnum;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoleRepository extends CrudRepository<Role, Integer> {
Optional<Role> findByName(RoleEnum name);
}
Re-run the application and ensure the roles table has been created in
the database.
Store the pre-defined roles in the database
We already know roles to persist in the system. So, before creating a
user, we must ensure the role exists in the database.
We will create a function executed at the application startup to create
roles in the database if they don't exist.
Spring Boot allows executing some actions on the application startup;
we will use it here, so let's create a package called "bootstrap", then
create the file "RoleSeeder.java" and add the code below:
package com.tericcabrel.authapi.bootstrap;
import com.tericcabrel.authapi.entities.Role;
import com.tericcabrel.authapi.entities.RoleEnum;
import com.tericcabrel.authapi.repositories.RoleRepository;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class RoleSeeder implements ApplicationListener<ContextRefreshedEvent> {
private final RoleRepository roleRepository;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
this.loadRoles();
}
Arrays.stream(roleNames).forEach((roleName) -> {
Optional<Role> optionalRole = roleRepository.findByName(roleName
optionalRole.ifPresentOrElse(System.out::println, () -> {
Role roleToCreate = new Role();
roleToCreate.setName(roleName)
.setDescription(roleDescriptionMap.get(roleName));
roleRepository.save(roleToCreate);
});
});
}
}
Re-run the application and verify the roles have been created in the
database.
@OneToOne(cascade = CascadeType.REMOVE)
@JoinColumn(name = "role_id", referencedColumnName = "id", nullable = false)
private Role role;
public Role getRole() {
return role;
}
Re-run the application and ensure the users' table has been updated.
💡 Make sure to empty the user table; otherwise, the column addition will fail
because the "role_id" column is not nullable.
Set the role when creating a user
The user role is now required, so creating a user without one will throw
an error. We must update signup() function in the
AuthenticationService.java; replace the function with the code below:
if (optionalRole.isEmpty()) {
return null;
}
return userRepository.save(user);
}
💡 We don't throw a runtime exception when the role is not found for brevity.
Re-run the application and try to register a user; we can see that the
user role is returned in the response.
Registering a user returns his role in the API response.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority( "ROLE_" +
return List.of(authority);
}
This allows us to access the user role from the authentication context.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {
// The existing code configuration here...
}
import org.springframework.security.access.prepost.PreAuthorize;
@RequestMapping("/users")
@RestController
public class UserController {
private final UserService userService;
@GetMapping("/me")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<User> authenticatedUser() {
// Existing code here...
}
@GetMapping
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
public ResponseEntity<List<User>> allUsers() {
// Existing code here...
}
}
That's it! Spring Security will handle the rest under the hood.
package com.tericcabrel.authapi.services;
import com.tericcabrel.authapi.dtos.RegisterUserDto;
import com.tericcabrel.authapi.entities.Role;
import com.tericcabrel.authapi.entities.RoleEnum;
import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.repositories.RoleRepository;
import com.tericcabrel.authapi.repositories.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
userRepository.findAll().forEach(users::add);
return users;
}
if (optionalRole.isEmpty()) {
return null;
}
return userRepository.save(user);
}
}
package com.tericcabrel.authapi.controllers;
import com.tericcabrel.authapi.dtos.RegisterUserDto;
import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.services.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/admins")
@RestController
public class AdminController {
private final UserService userService;
@PostMapping
@PreAuthorize("hasRole('SUPER_ADMIN')")
public ResponseEntity<User> createAdministrator(@RequestBody RegisterUserDto registe
User createdAdmin = userService.createAdministrator(registerUserDto
return ResponseEntity.ok(createdAdmin);
}
}
package com.tericcabrel.authapi.bootstrap;
import com.tericcabrel.authapi.dtos.RegisterUserDto;
import com.tericcabrel.authapi.entities.Role;
import com.tericcabrel.authapi.entities.RoleEnum;
import com.tericcabrel.authapi.entities.User;
import com.tericcabrel.authapi.repositories.RoleRepository;
import com.tericcabrel.authapi.repositories.UserRepository;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AdminSeeder implements ApplicationListener<ContextRefreshedEvent> {
private final RoleRepository roleRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AdminSeeder(
RoleRepository roleRepository,
UserRepository userRepository,
PasswordEncoder passwordEncoder
){
this.roleRepository = roleRepository;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
this.createSuperAdministrator();
}
if (optionalRole.isEmpty() || optionalUser.isPresent()) {
return;
}
Wrap up
In this post, we saw how to implement a Role Based Access Control in a
Spring Boot application, and here are the main steps to remember:
Create the role entity and data access layer.
Associate the user entity with a role.
Expose the user's role in the authentication context.
Enable the method security Spring security.
Protect the API route using the method security route
isAuthenticated() , hasRole() and hasAnyRole() .
To learn more about the method security of Spring Security and other
methods, you can such as hasAuthority() , check out the
documentation link.
You can find the code source on the GitHub repository.
Follow me on Twitter or subscribe to my newsletter to avoid missing the
upcoming posts and the tips and tricks I occasionally share.