Role-Based Access Control with Spring Security
Learn how to implement Role-Based Access Control (RBAC) in Spring Boot using Spring Security. Understand users, roles, permissions, URL security, method-level security, database-based roles, BCrypt, and real-world enterprise examples.
Introduction
Role-Based Access Control, commonly called RBAC, is one of the most widely used authorization models in enterprise Java applications.
Authentication answers:
Who are you?
Authorization answers:
What are you allowed to access?
RBAC solves authorization by assigning permissions through roles.
Example:
- Customer can view own profile
- Admin can manage users
- Manager can approve requests
- Auditor can view reports only
In this article, we will implement RBAC step by step using Spring Boot and Spring Security.
1. What is RBAC?
RBAC stands for Role-Based Access Control.
In RBAC, users are assigned roles.
Roles are allowed to access specific resources.
flowchart LR
User["User"]
--> Role["Role"]
--> Permission["Permission"]
--> Resource["Protected Resource"]
Example:
User: venu
Role: USER
Permission: View Profile
Resource: /api/user/profile
2. Why RBAC is Important?
Without RBAC, every user may access every API.
That is dangerous.
flowchart TB
User["Normal User"]
--> AdminAPI["Admin API"]
--> Risk["Security Risk"]
With RBAC:
flowchart TB
User["Normal User"]
--> RoleCheck["Role Check"]
RoleCheck -- USER --> UserAPI["User APIs"]
RoleCheck -- ADMIN --> AdminAPI["Admin APIs"]
RoleCheck -- Not Allowed --> Denied["403 Forbidden"]
RBAC helps with:
- API protection
- Data security
- Least privilege
- Compliance
- Separation of duties
3. Real-World Banking Example
Imagine a banking application.
| Role | Access |
|---|---|
| CUSTOMER | View accounts, download statements |
| MANAGER | Approve transactions |
| ADMIN | Manage users and roles |
| AUDITOR | View reports only |
flowchart TB
BankingApp["Banking Application"]
BankingApp --> Customer["CUSTOMER"]
BankingApp --> Manager["MANAGER"]
BankingApp --> Admin["ADMIN"]
BankingApp --> Auditor["AUDITOR"]
Customer --> ViewAccount["View Account"]
Manager --> Approve["Approve Transactions"]
Admin --> ManageUsers["Manage Users"]
Auditor --> Reports["View Reports"]
4. RBAC Flow in Spring Security
sequenceDiagram
participant Client
participant SecurityFilter
participant AuthenticationManager
participant AuthorizationManager
participant Controller
Client->>SecurityFilter: HTTP Request
SecurityFilter->>AuthenticationManager: Verify User
AuthenticationManager-->>SecurityFilter: Authenticated User
SecurityFilter->>AuthorizationManager: Check Role
alt Role Allowed
AuthorizationManager-->>Controller: Forward Request
else Role Not Allowed
AuthorizationManager-->>Client: 403 Forbidden
end
5. Project Setup
Maven Dependencies
<dependencies>
<!-- REST APIs -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
6. Project Structure
rbac-demo
└── src
└── main
└── java
└── com.codewithvenu.rbac
├── controller
│ └── DemoController.java
├── security
│ ├── SecurityConfig.java
│ └── UserConfig.java
└── RbacDemoApplication.java
7. Step 1: Create Test APIs
package com.codewithvenu.rbac.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/api/public/health")
public String health() {
return "Application is running";
}
@GetMapping("/api/customer/accounts")
public String customerAccounts() {
return "Customer account details";
}
@GetMapping("/api/manager/approvals")
public String managerApprovals() {
return "Manager transaction approvals";
}
@GetMapping("/api/admin/users")
public String adminUsers() {
return "Admin user management";
}
@GetMapping("/api/auditor/reports")
public String auditorReports() {
return "Auditor reports";
}
}
Code Explanation
/api/public/health
Public API. No login required.
/api/customer/accounts
Only CUSTOMER role should access.
/api/manager/approvals
Only MANAGER role should access.
/api/admin/users
Only ADMIN role should access.
/api/auditor/reports
Only AUDITOR role should access.
8. Step 2: Create In-Memory Users
package com.codewithvenu.rbac.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class UserConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails customer = User.withUsername("customer")
.password("{noop}customer123")
.roles("CUSTOMER")
.build();
UserDetails manager = User.withUsername("manager")
.password("{noop}manager123")
.roles("MANAGER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}admin123")
.roles("ADMIN")
.build();
UserDetails auditor = User.withUsername("auditor")
.password("{noop}auditor123")
.roles("AUDITOR")
.build();
return new InMemoryUserDetailsManager(
customer,
manager,
admin,
auditor
);
}
}
Important Note
{noop} means no password encoder.
This is only for learning.
In production, use BCrypt.
9. Step 3: Create Security Configuration
package com.codewithvenu.rbac.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/customer/**")
.hasRole("CUSTOMER")
.requestMatchers("/api/manager/**")
.hasRole("MANAGER")
.requestMatchers("/api/admin/**")
.hasRole("ADMIN")
.requestMatchers("/api/auditor/**")
.hasRole("AUDITOR")
.anyRequest()
.authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
Code Explanation
requestMatchers("/api/public/**").permitAll()
Allows public APIs without authentication.
hasRole("CUSTOMER")
Allows only users with role CUSTOMER.
Spring internally checks for authority ROLE_CUSTOMER.
hasRole("ADMIN")
Allows only users with role ADMIN.
anyRequest().authenticated()
Any unmatched API requires login.
httpBasic()
Enables Basic Authentication for learning.
10. Spring Role Prefix Rule
Important Spring Security rule:
roles("ADMIN")
internally becomes:
ROLE_ADMIN
So this:
.hasRole("ADMIN")
checks for:
ROLE_ADMIN
Do not write:
.hasRole("ROLE_ADMIN")
That is a common mistake.
11. Access Control Matrix
| API | CUSTOMER | MANAGER | ADMIN | AUDITOR |
|---|---|---|---|---|
/api/public/health |
✅ | ✅ | ✅ | ✅ |
/api/customer/accounts |
✅ | ❌ | ❌ | ❌ |
/api/manager/approvals |
❌ | ✅ | ❌ | ❌ |
/api/admin/users |
❌ | ❌ | ✅ | ❌ |
/api/auditor/reports |
❌ | ❌ | ❌ | ✅ |
12. Authorization Decision Flow
flowchart TB
Request["HTTP Request"]
--> SecurityFilter["Spring Security Filter"]
SecurityFilter --> Authenticated{"Authenticated?"}
Authenticated -- No --> Unauthorized["401 Unauthorized"]
Authenticated -- Yes --> RoleCheck{"Has Required Role?"}
RoleCheck -- Yes --> Controller["Controller"]
RoleCheck -- No --> Forbidden["403 Forbidden"]
13. Test APIs
Public API
curl http://localhost:8080/api/public/health
Expected:
Application is running
Customer API
curl -u customer:customer123 http://localhost:8080/api/customer/accounts
Expected:
Customer account details
Manager API
curl -u manager:manager123 http://localhost:8080/api/manager/approvals
Expected:
Manager transaction approvals
Admin API
curl -u admin:admin123 http://localhost:8080/api/admin/users
Expected:
Admin user management
Auditor API
curl -u auditor:auditor123 http://localhost:8080/api/auditor/reports
Expected:
Auditor reports
14. Forbidden Access Example
Customer trying to access admin API:
curl -u customer:customer123 http://localhost:8080/api/admin/users
Expected:
403 Forbidden
Why?
Customer is authenticated but not authorized.
flowchart LR
Customer["CUSTOMER Role"]
--> AdminAPI["ADMIN API"]
--> Forbidden["403 Forbidden"]
15. 401 vs 403
| Status | Meaning |
|---|---|
| 401 Unauthorized | User is not authenticated |
| 403 Forbidden | User is authenticated but not authorized |
flowchart TB
Request --> LoginCheck{"Logged In?"}
LoginCheck -- No --> E401["401 Unauthorized"]
LoginCheck -- Yes --> RoleCheck{"Correct Role?"}
RoleCheck -- No --> E403["403 Forbidden"]
RoleCheck -- Yes --> Success["200 OK"]
16. Using hasAnyRole
Sometimes multiple roles can access the same API.
Example:
.requestMatchers("/api/reports/**")
.hasAnyRole("MANAGER", "ADMIN", "AUDITOR")
This means MANAGER, ADMIN, or AUDITOR can access reports.
flowchart LR
ReportsAPI["Reports API"]
ReportsAPI --> Manager["MANAGER"]
ReportsAPI --> Admin["ADMIN"]
ReportsAPI --> Auditor["AUDITOR"]
17. Using Authorities Instead of Roles
Roles are high-level.
Authorities are more fine-grained permissions.
Example:
ROLE_ADMIN
USER_READ
USER_WRITE
REPORT_VIEW
flowchart TB
User["User"]
User --> Role["ROLE_ADMIN"]
Role --> Permission1["USER_READ"]
Role --> Permission2["USER_WRITE"]
Role --> Permission3["REPORT_VIEW"]
18. Role vs Permission
| Role | Permission |
|---|---|
| ADMIN | USER_CREATE |
| ADMIN | USER_DELETE |
| MANAGER | TRANSACTION_APPROVE |
| AUDITOR | REPORT_VIEW |
Roles group permissions.
Permissions describe actions.
19. Method-Level Security
URL-level security protects endpoints.
Method-level security protects service methods.
Enable method security:
package com.codewithvenu.rbac.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
20. Service with Method Security
package com.codewithvenu.rbac.service;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
@PreAuthorize("hasRole('CUSTOMER')")
public String viewAccounts() {
return "Customer accounts";
}
@PreAuthorize("hasRole('MANAGER')")
public String approveTransaction() {
return "Transaction approved";
}
@PreAuthorize("hasRole('ADMIN')")
public String deleteUser() {
return "User deleted";
}
}
Why Method Security?
Because not every method is called only from REST APIs.
Methods can be called from:
- Controllers
- Scheduled jobs
- Message listeners
- Internal services
- GraphQL resolvers
So service-level protection is useful.
21. Method Security Flow
flowchart TB
Controller["Controller"]
--> ServiceMethod["Service Method"]
ServiceMethod --> PreAuthorize["@PreAuthorize"]
PreAuthorize --> RoleCheck{"Role Allowed?"}
RoleCheck -- Yes --> Execute["Execute Method"]
RoleCheck -- No --> Denied["Access Denied"]
22. Database-Based RBAC
In real applications, users and roles usually come from a database.
flowchart TB
UserTable["users table"]
--> UserRoles["user_roles table"]
--> RolesTable["roles table"]
RolesTable --> PermissionsTable["permissions table"]
23. Database Table Design
users
| id | username | password |
|---|---|---|
| 1 | venu | encrypted/hash |
| 2 | admin | encrypted/hash |
roles
| id | name |
|---|---|
| 1 | CUSTOMER |
| 2 | ADMIN |
user_roles
| user_id | role_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
24. Database-Based Entity Design
@Entity
@Table(name = "users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ElementCollection(fetch = FetchType.EAGER)
private Set<String> roles = new HashSet<>();
}
For beginners, @ElementCollection is simpler than a full many-to-many role table.
25. Database-Based UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final AppUserRepository repository;
public CustomUserDetailsService(AppUserRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String username) {
AppUser user = repository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
String[] roles = user.getRoles()
.toArray(new String[0]);
return User.withUsername(user.getUsername())
.password(user.getPassword())
.roles(roles)
.build();
}
}
26. Repository
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
}
27. BCrypt Password Encoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Use BCrypt to store passwords securely.
Never use plain text passwords.
28. Production RBAC Architecture
flowchart TB
Client["Client"]
--> API["Spring Boot API"]
API --> Security["Spring Security"]
Security --> UserDetailsService["UserDetailsService"]
UserDetailsService --> DB["User / Role DB"]
Security --> Authorization["Role Check"]
Authorization --> Controller["Controller"]
Controller --> Service["Business Service"]
29. Best Practices
- Use least privilege
- Avoid giving everyone ADMIN role
- Use roles for broad access
- Use permissions for fine-grained access
- Protect both URL and service methods
- Store users and roles in database
- Use BCrypt for passwords
- Use JWT or OAuth2 for production APIs
- Log access-denied events
- Review roles regularly
30. Common Mistakes
| Mistake | Problem |
|---|---|
| Everyone has ADMIN role | High security risk |
| Only frontend hides buttons | Backend APIs still exposed |
| No method-level security | Internal calls may bypass protection |
| Hardcoded users in production | Not scalable |
| Plain text passwords | Data breach risk |
| Confusing role and authority | Broken access rules |
31. Interview Questions
Q1. What is RBAC?
RBAC means Role-Based Access Control. Access is granted based on assigned roles.
Q2. What is a role?
A role represents a responsibility or access level, such as ADMIN, USER, or MANAGER.
Q3. What is the difference between role and permission?
A role groups multiple permissions. A permission represents a specific action.
Q4. What does hasRole("ADMIN") check internally?
It checks for authority ROLE_ADMIN.
Q5. What is the difference between 401 and 403?
401 means unauthenticated. 403 means authenticated but not authorized.
Q6. Why use method-level security?
It protects service methods even if they are called from different entry points.
Q7. What annotation enables method-level security?
@EnableMethodSecurity.
Q8. Which annotation is used for method authorization?
@PreAuthorize.
32. Key Takeaways
- RBAC is used for authorization.
- Users are assigned roles.
- Roles define access to APIs and operations.
- Spring Security supports URL-based and method-level RBAC.
hasRole("ADMIN")checks forROLE_ADMIN.- 401 means unauthenticated.
- 403 means authenticated but forbidden.
- Production applications should store users and roles in a database.
- Use BCrypt for password hashing.
- Use least privilege for all roles.
Next Article
➡️ JWT Authentication in Spring Boot