Role-based authorization, as its name suggests, is an authorization mechanism where user permissions are determined by the roles they are assigned in a system. The user gets assigned a role that determines what level of authorization he is granted. The preceding article in this series ended with a relatable analogy. To reemphasize my point, I will use a more high-tech fancy analogy; consider your favorite spy movies with different access levels for agents and directors.
This article will focus on step by step implementation of role-based authorization. I will introduce method-level role-based authorization before committing to database authentication.
Table of Contents
Pre-requisites
Role-based Authorization
Method-level
Database Authentication
Integration and configuration
Pre-requisites
This is the fourth part in the series, I recommend going through the previous article in the series.
It is recommended that you are comfortable with Java and Springboot. Basic knowledge of security principles, including authentication, authorization, and encryption, is necessary.
The first article provides a general overview of the Spring security architecture.
In the second part, we take a great look at the default security configuration provided by spring security 6 and in-memory authentication.
The third article contains a detailed implementation of in-memory authentication.
Implementing Role-based Authorization
Role-based authorization can be implemented with in-memory authentication as well as database authentication. The image above shows a simplified overview of key players in spring security. Remember, in-memory authentication allows us to make use of an internal database. In this case, user profiles are created inside of the UserDetailsService
method which returns a type User
which is then saved in an instance of InMemoryUserDetailsManager
(see image below).
Step 1: Declare the role of the User instance created.
Notice how firstUser
and secondUser
objects
have a role of type “USER” and “ADMIN” assigned to them respectively.
Step 2: Define the access level in the routes in the Security Filter Chain
Remember, that routes defined in our controller classes are either permitted to all clients without authentication. Such routes can be the homepage, signup, or login page of our application. It follows that anyone should be able to load up this page on their browsers.
Every other route will require authentication. Furthermore, we define certain routes to be only accessible by users that have a specific role. So the admin dashboard in our application should only be accessed by the admin. See line 39 in the image above.
A detailed observation of the image above shows that a route can be defined for a particular role type, in which case,
hasRole
is used. On the otherhasAnyRole
is used for multiple user types separated by a comma.
Side-note
It is common to find,
hasAuthority
andhasAnyAuthority
. Some developers also save roles as “ROLE_USER”. Note thathasRole
andhasAuthority
can be used interchangeably. It is great coding practice to maintain a standard throughout your application. This promotes readability, maintainability, and ease of understanding for yourself and other developers working on the project.
Implementing Method-level Role-based Authorization
Method-level role-based authorization simply means defining roles in individual methods present in our application’s controller classes.
Essentially the methods in our controller class represent distinct routes where the user can make requests at. So instead of specifying the route path in the Security filter chain as previously done, we simply indicate in the respective methods in the controller that this method should be accessed by a user of a given role type.
Step 1: Annotate the SecurityConfig class with
@EnableMethodSecurity
Line 21 on the image above shows the annotation affixed to the SecurityConfig class.
Notice how there is no need to specify role-based authorization in the SecurityFilterChain hence lines 39 -lines 42 are commented out.
Step 2: Annotate the methods in the controller
Each method in the controller class is annotated with
@PreAuthorize
By indicating
hasRole, hasAuthority, hasAnyRole, hasAnyAuthority
and specifying the role type, we implement role-based authorization.
It is evident that setting up role-based authorization is pretty straightforward whether the configuration is done in the SecurityFilterChain
or by using the annotations in the methods in the controller class.
In practice, using a combination of method-level role-based authorization and the security filter chain helps create a robust security framework. Method-level authorization provides fine-grained control over individual methods, while the security filter chain ensures broader protection at the request level. Together, they work harmoniously to enhance the overall security of an application.
Database Authentication
The interesting thing about spring security is how the different implementations build on each other. In a previous article, I showed in-memory authentication to be a kind of middleman between the default authentication and database authentication. Database Authentication simply means that the users are saved on an external database. This is suitable for a larger number of users compared to that which in-memory authentication can comfortably handle.
The key players remain the same and they perform the kind of functions expected of them, however, in implementing database authentication, some changes are made to how these key players perform their functions and how they interact with each other.
For one, in our Springboot project, we will require the creation of
User entity, User repository, User Service etc.
Role entity or a Role class that exists as an enum type.
A
CustomUserDetailsService
class that implements the defaultUserDetailsService.
A
CustomUserDetails
class that implements UserDetails. It is to be noted that this particular class is not neccessary as the CustomUserDetailsService will return a User of type UserDetails by default.
As earlier mentioned, understanding spring boot is a crucial pre-requisite so I will not be spending time explaining how entities are created or how relationships are established between the User entity and the role entity.
Step 1: Create User and Role entity, establish relationship between entities. Setup the repositories extending JpaRepository.
Step 2: Create CustomUserDetailsService Class
The class is annotated with
@Service
. It is also annotated with@AllArgsConstructor
indicating constructor-based dependency injection.Implementing
UserDetailsService
in our custom class requires the class to implement theloadUserByUsername
method. This methodfetches a user from the database via the
userRepository.findBy
method. An exception is thrown if the username passed in as an argument does not match any username in our database.Once a username match is made, the role type assigned to that user is mapped into an instance of
SimpleGrantedAuthority
provided by spring security framework.The user is finally returned as a User which is actually a type of
UserDetails
provided by spring framework.
Step 3: Inject the CustomUserDetailsService class into the SecurityConfig class
The CustomUserDetailsService class is implemented by constructor-based dependency injection.
Observe line 49. The inbuilt UserDetailsService which was used for in-memory Authentication is no longer needed hence it is commented out.
It should be established that the interaction between the key players still stands. The Authentication manager via the authentication provider works behind the scenes to make use of the CustomUserDetailsService.
Integration and configuration
So far, we have implemented the crucial requirements of Database Authentication; creating user and role entities, and a custom class that implements UserDetailsService.
Before we proceed, a glance at the big-picture of authentication and authorization.
Consider Instagram; whether you intend to make friends, reach more people, advertise your business or just have fun. You are required to sign up i.e. create an account or login if you are already registered as a user.
Database Authentication caters to these two needs, we thus have to define routes in our controller that handle registration of user as well as user login. It is when a logged-in user tries to make a request at another route, that we come to see the full implementation of the security architecture we have created.
Step 1: Create a controller to handle registration and login routes.
The controller is typically called AuthController. This controller will have an AuthService injected into it. In the AuthService, the registration and login logic is clearly spelt out.
PS: It is appropriate that logic is defined in the service layer, however for the purpose of this lecture, I am implementing the logic in the controller layer.
The logic is pretty straightforward. When a user chooses to register on our application, we make checks to determine if the username and email supplied by this user is unique and has not been used previously by another user.
If unique, we go ahead to create an instance of
UserEntity
and save this user to the database via the save method ofUserRepository
Notice how the password is saved using a
passwordEncoder.
Notice also the assigning of a default role of type ‘USER’ to the client who is about to register.
For login, the
loginDto
argument passed into the login method represents the user credentials i.e. username or email and password as the case may be. In this case, we are using the username and password to initiate a login.If the username and password are valid, an
Authentication
object is created.SecurityContextHolder
is then set. Consider SecurityContextHolder as a list that is updated with authenticated users. Because the user provided valid credentials, he is added to the list and can subsequently access other endpoints in our application, so far, he is eligible to access it i.e. role-based authorization.
Step 2: Configure the SecurityFilterChain
On line 37, the auth controller that contains the login and register routes is set to
.permitAll()
This is because this route should be accessible to all clients making a request at our controller.
Note that the HttpMethod is a post method. This is because the client will be supplying information to fields provided by the front end.
Notice also that asterisks appear after the forward-slash. This is a way of saying that all routes inside of ‘auth’ will be permitted to all.
Step 3: Testing is done with Postman which acts as a kind of front-end client.
The URL is specified. The ‘form’ is duly filled; upon pressing the send button, we get a response from the backend. In this case, it is a ‘User successfully registered’ response.
The URL is specified. The ‘form’ is duly filled; upon pressing the send button, we get a response from the backend. In this case, it is a ‘User logged in successfully’ response.
In the context above, the endpoints i.e. URLs have been defined in the
securityFilterChain
as routes permitted to all clients.For routes that will require authentication, such as a testing controller. The User is required to re-authenticate before accessing the route.
The route in the image above requires authentication. Only a logged-in user should access this route. In this case, we have to enter the user credentials before making a request at this route.

Upon inputting valid user credentials, the client is able to access the intended route. This presents a valid drawback in database authentication.
The drawback of database authentication is observed when testing routes that require authentication i.e. routes that are not permitted to all.
Imagine visiting Instagram, after signing up or logging in as the case may be, you are directed to the home page. Imagine trying to add a friend, make a post, or do something of interest, and Instagram requires you to ‘authenticate’ yourself again. This means for every action done on the application, you have to provide your username and password. Well, this is the case with database Authentication.
However, constantly re-authenticating for every action can be cumbersome and disrupts the user flow. Ideally, once a user has logged in successfully, subsequent requests or actions should be authorized automatically without requiring the user to provide their credentials repeatedly.
To overcome the inconvenience of constant re-authentication in web applications using database authentication, techniques such as session management or token-based authentication are commonly employed. These methods generate tokens (session or authentication tokens) during the initial login, which are stored and used to authenticate subsequent requests. By utilizing these tokens, users can perform actions without having to repeatedly enter their username and password. This significantly enhances the user experience by eliminating the need for frequent re-authentication.
Wow, it’s been a ride!
In conclusion, the progression from default configuration to in-memory authentication and database authentication in Spring Security is seamless and enables developers to enhance the authentication capabilities of their applications.
With the introduction of Spring Security 6, implementing these steps becomes even simpler. By following this progression, developers can easily evolve and customize their authentication solutions, building robust and scalable applications. Spring Security's clear guidelines and powerful features provide developers with the tools they need to efficiently implement secure authentication mechanisms. Overall, Spring Security empowers developers to focus on delivering secure applications while ensuring a seamless and enjoyable user experience.