Role-Based Access Control (RBAC) in Spring Security with Kotlin
Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security

Role-Based Access Control (RBAC) is a crucial security mechanism that restricts system access based on the roles of individual users. In this comprehensive guide, we'll implement RBAC in a Spring Boot application using Kotlin, covering everything from basic setup to advanced configurations.
Introduction
When building enterprise applications, you often need different levels of access for different types of users. For example:
- Administrators who can manage user accounts
- Managers who can view reports
- Regular users who can only access their own data
RBAC provides a clean, scalable way to manage these permissions. We'll build a complete example showing how to implement this in Spring Boot with Kotlin.
Prerequisites
To follow this tutorial, you'll need:
- Kotlin 1.9.0 or later
- Spring Boot 3.2.0 or later
- JDK 17 or later
- Basic understanding of Spring Security concepts
- Familiarity with Kotlin syntax
Project Setup
First, create a new Spring Boot project with the following dependencies in your build.gradle.kts
:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// For demonstration purposes, we'll use H2 database
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}
Implementing the Core RBAC Components
1. Define the Role Enum
First, let's define our roles:
enum class Role {
ROLE_USER,
ROLE_MANAGER,
ROLE_ADMIN
}
2. Create the User Entity
Next, we'll create our user entity with role support:
@Entity
@Table(name = "users")
class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(unique = true)
val username: String,
val password: String,
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = [JoinColumn(name = "user_id")])
@Enumerated(EnumType.STRING)
val roles: Set<Role> = setOf(Role.ROLE_USER)
)
3. Implement UserDetailsService
Create a service to load user details:
@Service
class UserDetailsServiceImpl(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
val user = userRepository.findByUsername(username)
?: throw UsernameNotFoundException("User not found")
return org.springframework.security.core.userdetails.User
.withUsername(user.username)
.password(user.password)
.roles(*user.roles.map { it.name.removePrefix("ROLE_") }.toTypedArray())
.build()
}
}
4. Configure Security
Set up the security configuration:
@Configuration
@EnableWebSecurity
class SecurityConfig(
private val userDetailsService: UserDetailsServiceImpl
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasRole("MANAGER")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
}
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.userDetailsService(userDetailsService)
return http.build()
}
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
}
Creating Protected Resources
Let's create some endpoints with different role requirements:
@RestController
@RequestMapping("/api")
class UserController {
@GetMapping("/public/info")
fun getPublicInfo() = "This is public information"
@GetMapping("/user/profile")
@PreAuthorize("hasRole('USER')")
fun getUserProfile(authentication: Authentication): String {
return "User profile for: ${authentication.name}"
}
@GetMapping("/manager/reports")
@PreAuthorize("hasRole('MANAGER')")
fun getManagerReports(): String {
return "Manager reports"
}
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
fun getAdminUserList(): String {
return "Admin user list"
}
}
Method-Level Security
Spring Security also supports method-level security. Here's how to implement it:
@Configuration
@EnableMethodSecurity
class MethodSecurityConfig
@Service
class UserService {
@PreAuthorize("hasRole('ADMIN')")
fun deleteUser(userId: Long) {
// Delete user logic
}
@PreAuthorize("hasRole('MANAGER') or @userSecurity.isOwner(#userId)")
fun updateUser(userId: Long, userDetails: UserDetails) {
// Update user logic
}
}
Custom Security Expressions
You can create custom security expressions for complex authorization rules:
@Component("userSecurity")
class UserSecurityEvaluator(
private val userRepository: UserRepository
) {
fun isOwner(userId: Long): Boolean {
val authentication = SecurityContextHolder.getContext().authentication
val username = authentication.name
return userRepository.findByIdAndUsername(userId, username) != null
}
}
Testing RBAC Implementation
Here's how to test your RBAC implementation:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
@WithMockUser(roles = ["ADMIN"])
fun `admin can access admin endpoints`() {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk)
}
@Test
@WithMockUser(roles = ["USER"])
fun `user cannot access admin endpoints`() {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden)
}
}
Best Practices
- Role Hierarchy
- Implement role hierarchies for complex permissions:
@Bean
fun roleHierarchy(): RoleHierarchy {
val hierarchy = RoleHierarchyImpl()
hierarchy.setHierarchy("""
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
""".trimIndent())
return hierarchy
}
- Dynamic Roles
- Store roles in a database for runtime flexibility
- Implement a caching strategy for role checks
- Security Auditing
- Enable security auditing to track access:
@EnableJpaAuditing
class AuditConfig {
@Bean
fun auditorAware(): AuditorAware<String> {
return AuditorAware {
Optional.ofNullable(SecurityContextHolder.getContext().authentication?.name)
}
}
}
Common Pitfalls to Avoid
- Role Prefixes: Always use the
ROLE_
prefix in database but remove it when usinghasRole()
- Permission Granularity: Don't create too many roles; use permissions for fine-grained control
- Security Context: Be careful when accessing SecurityContext in async operations
- Role vs Authority: Understand the difference between roles and authorities
Conclusion
RBAC is a powerful way to manage access control in your Spring Boot applications. By following this guide, you've learned how to:
- Implement basic role-based security
- Configure method-level security
- Create custom security expressions
- Test your RBAC implementation
- Apply best practices for production use
Role-Based Access Control is commonly used alongside JWT authentication. If you haven't set up authentication yet, check out our guide on [implementing JWT authentication in Spring Boot](https://kotlincraft.dev/articles/jwt-authentication-in-spring-boot-with-kotlin-from-zero-to-production). For API-only applications, you might also want to review our guide on [CSRF protection in REST APIs](https://kotlincraft.dev/articles/why-you-should-disable-csrf-protection-for-rest-apis-in-spring-boot-and-how-to-do-it-right) to ensure your security configuration is optimized for your use case.