Implementing a Module-Based Authorization Approach in Spring Boot
In this practical tutorial, I will show you how to implement an authorization feature in Spring Boot.
This approach is useful in applications where you want to assign different menus to specific roles. When a user logs in to the application, they will only see the menus assigned to their role.
Step 1: Identify the Required Entity Models
This approach requires the following entity models:
- Users.java
- Role.java
- RoleUser.java
- Module.java
- RoleModule.java
Let me explain the purpose of each of these models.
Users.java: This model stores the application's users.
Role.java: This model stores all the roles available in the application, such as Admin, Manager, Accountant, and Storekeeper.
RoleUser.java: This model stores the roles assigned to users. For example, a user can be assigned the role of an Accountant, Storekeeper, or Manager. The assigned roles are stored in this model.
Module.java: This model stores all the menus available in the application, including their labels and routes. For example, in a typical inventory management application, you might have menus such as Add Product and List Products, with routes like /add-product and /list-products, respectively.
RoleModule.java: This model stores the menus assigned to specific roles. For example, an administrator can assign the Add Product menu to users with the Storekeeper role.
Step 2: Create the Models
In this step, I created the models, defined their attributes and data types, and established the necessary relationships between them.
Users model:
package com.inventory.example.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 45)
private String userName;
@Column(nullable = false, length = 225)
private String password;
@Column(nullable = false, length = 45, columnDefinition = "VARCHAR(45) DEFAULT '0'")
}
Role model:
package com.inventory.example.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 45)
private String roleName;
}
RoleUser model:
package com.inventory.example.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class RoleUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private Users user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)
private Role role;
}
Module model: I renamed this as AppModule because the name is conflicting with the language inbuild Module class
package com.inventory.example.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "modules")
public class AppModule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
private String route;
private String description;
private Boolean active = true;
}
RoleModule model:
package com.inventory.example.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "role_module")
public class RoleModule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)
private Role role;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "module_id", nullable = false)
private AppModule module;
}
Step 3: Create the Repository Interfaces
In this step, I created repository interfaces for each model by extending the JpaRepository interface.
UsersRepository.class
package com.inventory.example.repository;
import com.inventory.example.model.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<Users, Long> {
Optional<Users> findByUserName(String userName);
}
RoleRepository.class
package com.inventory.example.repository;
import com.inventory.example.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleRepository extends JpaRepository<Role,Long> {
}
RoleUserRepository.class
package com.inventory.example.repository;
import com.inventory.example.model.RoleUser;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface RoleUserRepository extends JpaRepository<RoleUser, Long> {
@EntityGraph(attributePaths = {"user", "role"})
@Query("""
SELECT ru
FROM RoleUser ru """)
List<RoleUser> findAllRoleUsers();
Optional<RoleUser> findByUser_Id(Long userId);
}
ModuleRepository.class
package com.inventory.example.repository;
import com.inventory.example.model.AppModule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ModuleRepository extends JpaRepository<AppModule, Long> {
}
RoleModuleRepository.class
package com.inventory.example.repository;
import com.inventory.example.model.RoleModule;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RoleModuleRepository extends JpaRepository<RoleModule, Long> {
@EntityGraph(attributePaths = {"module"})
List<RoleModule> findAllByRole_Id(Long roleId);
}
Step 4: Create the Service Classes
In this step, I created service classes for each model and injected the corresponding repositories using constructor-based dependency injection. For this tutorial I am going to focus on the Role, RoleUser, Module and RoleModule service classes. Remember to import the necessary imports and looking at the class, you will see that every service class as a correcsponding request dto.
RoleService.class
@Service
public class RoleService {
//Call the RoleRepository
private final RoleRepository roleRepository;
//constructor injection
public RoleService(RoleRepository roleRepository) {
this.roleRepository = roleRepository;
}
//method to save roles
public Role saveRole(RoleRequest roleRequest) {
Role role = new Role();
role.setRoleName(roleRequest.getRoleName());
return roleRepository.save(role);
}
//method to find all roles
public List<Role> findAll() {
return roleRepository.findAll();
}
…
}
RoleUserService.class
@Service
public class RoleUserService {
private final RoleUserRepository roleUserRepository;
private final RoleRepository roleRepository;
private final UserRepository usersRepository;
public RoleUserService(RoleUserRepository roleUserRepository, RoleRepository roleRepository, UserRepository usersRepository) {
this.roleUserRepository = roleUserRepository;
this.roleRepository = roleRepository;
this.usersRepository = usersRepository;
}
public RoleUser save(RoleUserRequest roleUserRequest) {
Users user = usersRepository.findById(roleUserRequest.getUserId())
.orElseThrow(() -> new RuntimeException(
"User not found with ID: " + roleUserRequest.getUserId()
));
Role role = roleRepository.findById(roleUserRequest.getRoleId())
.orElseThrow(() -> new RuntimeException(
"Role not found with ID: " + roleUserRequest.getRoleId()
));
RoleUser roleUser = new RoleUser();
roleUser.setUser(user);
roleUser.setRole(role);
return roleUserRepository.save(roleUser);
}
//method to find all roles
public List<RoleUser> findAll() {
return roleUserRepository.findAllRoleUsers();
}
//method to find roles by id
public Optional<RoleUser> findById(Long id) {
return roleUserRepository.findById(id);
}
…
}
ModuleService.class
@Service
public class ModuleService {
private final ModuleRepository moduleRepository;
public ModuleService(ModuleRepository moduleRepository) {
this.moduleRepository = moduleRepository;
}
public AppModule createModule(ModuleRequest moduleRequest) {
AppModule module = new AppModule();
module.setName(moduleRequest.getName());
module.setRoute(moduleRequest.getRoute());
module.setDescription(moduleRequest.getDescription());
return moduleRepository.save(module);
}
public List<AppModule> findAll() {
return moduleRepository.findAll();
}
public Optional<AppModule> findById(Long id) {
return moduleRepository.findById(id);
}
…
}
RoleModuleService.class
@Service
public class RoleModuleService {
private final RoleModuleRepository roleModuleRepository;
private final RoleRepository roleRepository;
private final ModuleRepository moduleRepository;
public RoleModuleService(RoleModuleRepository roleModuleRepository, RoleRepository roleRepository, ModuleRepository moduleRepository) {
this.roleModuleRepository = roleModuleRepository;
this.roleRepository = roleRepository;
this.moduleRepository = moduleRepository;
}
public List<RoleModule> createRoleModule(
RoleModuleRequest request) {
Role role = roleRepository.findById(request.getRoleId())
.orElseThrow(() -> new RuntimeException(
"Role not found with ID: " + request.getRoleId()
));
List<RoleModule> roleModules = new ArrayList<>();
for (Long moduleId : request.getModuleId()) {
AppModule module = moduleRepository.findById(moduleId)
.orElseThrow(() -> new RuntimeException(
"Module not found with ID: " + moduleId
));
RoleModule roleModule = new RoleModule();
roleModule.setRole(role);
roleModule.setModule(module);
roleModules.add(roleModule);
}
return roleModuleRepository.saveAll(roleModules);
}
public Optional<RoleModule> findRoleModuleById(Long id) {
return roleModuleRepository.findById(id);
}
public List<RoleModule> findAllRoleModules() {
return roleModuleRepository.findAll();
}
}
Step 5: Create the Controllers
In this step, I created controller classes for each model. These controllers expose the endpoints that the frontend can consume.
Finally, I created AuthController.java, which handles user authentication.
In the login method, the application validates the user's credentials and role, uses the role ID to fetch all the modules assigned to that role, and returns the modules to the frontend.
With this approach, the frontend can display only the menus assigned to the user's role.
AuthController.class
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "Login Management", description = "Apis for managing login")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserRepository userRepository;
private final RoleModuleRepository roleModuleRepository;
private final RoleUserRepository roleUserRepository;
@PostMapping("/login")
public ResponseEntity<ApiDataResponse<?>> login(
@RequestBody LoginRequest request
) {
try {
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserDetails userDetails =
(UserDetails) authentication.getPrincipal();
String token = jwtService.generateToken(userDetails);
// Get the authenticated user's database record.
var user = userRepository.findByUserName(
userDetails.getUsername()
).orElseThrow(() ->
new RuntimeException("Authenticated user not found")
);
// Default values when no role is assigned.
Role role = null;
List<AppModule> modules = new ArrayList<>();
// Check whether the user has a role assignment.
Optional<RoleUser> roleUserOptional =
roleUserRepository.findByUser_Id(user.getId());
if (roleUserOptional.isPresent()) {
RoleUser roleUser = roleUserOptional.get();
role = roleUser.getRole();
if (role != null) {
modules = roleModuleRepository
.findAllByRole_Id(role.getId())
.stream()
.map(RoleModule::getModule)
.toList();
}
}
// Generate the login response regardless of role assignment.
LoginResponse response = new LoginResponse(
token,
modules
);
return ResponseEntity.ok(
new ApiDataResponse<>(
true,
"You have successfully login!",
response
)
);
} catch (BadCredentialsException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(
new ApiDataResponse<>(
false,
"Invalid username or password",
null
)
);
}
}
}
Lessons Learned
- Learned how to implement module-based authorization using Spring Boot and JPA relationships.
- Understood how to assign roles to users and modules to roles.
- Learned how to return role-specific menus to the frontend using the login endpoint.
Challenges Faced and Resolved
- Managing entity relationships: Resolved by defining the appropriate relationships between users, roles, and modules.
- Retrieving role-specific menus: Resolved by using the role ID to fetch the modules assigned to the user's role during login.
- Organizing the application: Addressed by separating the implementation into models, repositories, services, and controllers.