Role Based Access Control (RBAC)
Role Based Access Control (RBAC) is a way of deciding what each user can do in a system based on the role they hold. Instead of giving permissions to every user one by one, you give permissions to a role, and then give that role to users.
In simple words, RBAC answers one question: "What is this person allowed to do?" A cashier, a manager and an administrator all log in to the same banking application, but each of them sees different screens and can perform different actions.
In this tutorial you will learn how RBAC works, the rules behind it, how it compares with other access control models, and how to build it step by step in a Spring Boot 3 application using Spring Security.
1. What Is RBAC?
RBAC (Role-Based Access Control) is a security mechanism where permissions are assigned to roles, and users are assigned to those roles.
Instead of giving permissions directly to every user, we manage access through roles.
2. Why Do We Need RBAC?
Imagine an application where every logged-in user can do everything. One wrong click could delete important data, and one stolen password could expose the whole system. Managing permissions user by user is not much better: with hundreds of users, it quickly becomes messy and error-prone.
RBAC fixes both problems:
- Better security: users get only the access their job needs.
- Easy management: change a role once, and every user with that role is updated.
- Fewer mistakes: new users get a ready-made, tested set of permissions.
- Simple audits: it is easy to answer "who can approve loans?" by looking at one role.
- Scales well: adding 1,000 new users does not mean 1,000 new permission setups.
3. Key Components of RBAC
| Component | Meaning | Example |
|---|---|---|
| User | A person or system that uses the application | Riya, Arjun, a reporting service |
| Role | A named job or responsibility | CUSTOMER, MANAGER, ADMIN |
| Permission | One allowed action on one kind of resource | ACCOUNT_READ, LOAN_APPROVE |
| User-Role assignment | Which roles each user has | Arjun → MANAGER |
| Role-Permission assignment | Which permissions each role has | MANAGER → LOAN_APPROVE |
The important idea is that users and permissions are never linked directly. They are always connected through roles: User → Role → Permission.
4. How RBAC Works
Every request goes through the same simple steps:
- Login: the user proves their identity.
- Load roles: the system finds the roles assigned to the user, and the permissions inside those roles.
- Check: for each request, the system asks, "Does this user have a role or permission that allows this action?"
- Decide: if yes, the request continues. If no, it is rejected with an "access denied" error (HTTP
403 Forbiddenin web APIs).
5. Example: Online Banking
Consider an online banking application with three roles. The table below is called a permission matrix. It shows at a glance what each role can do.
| Action | Customer | Manager | Admin |
|---|---|---|---|
| View own account and transactions | ✔ | ✔ | ✔ |
| Transfer money from own account | ✔ | ✘ | ✘ |
| View any customer's account | ✘ | ✔ | ✔ |
| Approve loans | ✘ | ✔ | ✔ |
| View branch reports | ✘ | ✔ | ✔ |
| Create or remove users | ✘ | ✘ | ✔ |
| Change system settings | ✘ | ✘ | ✔ |
Now we only assign roles to people:
- Riya → Customer
- Arjun → Manager
- The IT administrator → Admin
If the bank later decides managers may also view audit logs, you add that permission to the Manager role once, and every manager gets it immediately.
6. Core RBAC Principles
7.1 Least Privilege
Give each role only the permissions it truly needs, and nothing more. If a role does not need to delete data, it should not be able to. This limits the damage if an account is misused or stolen.
7.2 Separation of Duties
Split sensitive tasks between different roles so that no single person can complete them alone. For example, the person who creates a large payment should not be the same person who approves it.
7.3 Role Hierarchy
Senior roles can inherit the permissions of junior roles. For example, ADMIN includes everything MANAGER can do, and MANAGER includes everything CUSTOMER can do. This avoids copying the same permissions into several roles.
7. RBAC vs ACL vs ABAC
RBAC is not the only access control model. Here is how it compares with two other popular models:
| Model | Access Is Based On | Example Rule | Good For |
|---|---|---|---|
| ACL (Access Control List) | A list of users stored on each resource | "Riya and Arjun may edit this file" | File systems, shared documents |
| RBAC (Role Based) | The user's roles | "Managers may approve loans" | Most business applications |
| ABAC (Attribute Based) | Attributes of the user, resource and environment | "Managers may approve loans under ₹10 lakh during office hours in their own branch" | Complex, fine-grained rules |
Many real systems mix them: RBAC for the main rules, plus a few attribute checks such as "customers can only see their own account".
8. Roles and Authorities in Spring Security
Spring Security stores everything a user is allowed to do as a list of authorities (GrantedAuthority objects). Roles are simply authorities whose name starts with the prefix ROLE_.
| You Write | Spring Checks For Authority |
|---|---|
hasRole("ADMIN") | ROLE_ADMIN |
hasAnyRole("MANAGER", "ADMIN") | ROLE_MANAGER or ROLE_ADMIN |
hasAuthority("LOAN_APPROVE") | LOAN_APPROVE (exact name, no prefix) |
A common pattern is to use roles for broad access (who can enter an area) and permission authorities for specific actions (who can approve, delete or export).
9. Project Setup
Create a Spring Boot 3 project with the Spring Web and Spring Security starters. The test dependency adds helpers for testing secured endpoints.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
10. URL-Based RBAC
URL-Based RBAC (Role-Based Access Control) is one of the simplest ways to control access to different parts of an application.
In this approach, access is granted or denied based on the URL pattern and the user's role. We define which roles are allowed to access specific endpoints or groups of endpoints.
For example, an application might have the following URL structure:
/admin/**→ ADMIN only/manager/**→ ADMIN and MANAGER/user/**→ ADMIN, MANAGER, and USER
When a request arrives, Spring Security checks the requested URL and verifies whether the authenticated user has the required role.
package com.javabytechie.bank.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Disabled only because this demo is a stateless REST API without browser sessions
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasAnyRole("MANAGER", "ADMIN")
.requestMatchers("/api/customer/**").hasAnyRole("CUSTOMER", "MANAGER", "ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
return new InMemoryUserDetailsManager(
User.withUsername("riya")
.password(encoder.encode("riya123"))
.roles("CUSTOMER")
.build(),
User.withUsername("arjun")
.password(encoder.encode("arjun123"))
.roles("MANAGER")
.build(),
User.withUsername("admin")
.password(encoder.encode("admin123"))
.roles("ADMIN")
.build()
);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
A few things to notice:
- Rules are checked from top to bottom, and the first matching rule wins. Put specific paths before general ones.
.roles("MANAGER")stores the authorityROLE_MANAGERfor you.anyRequest().authenticated()is a safe default: any URL not listed still requires login.- Passwords are stored as BCrypt hashes, never as plain text.
11. Method-Level RBAC
URL-level authorization protects an entire group of endpoints. However, sometimes we need finer-grained access control, where authorization depends on the specific method being called.
Spring Security provides method-level security for this purpose. We can use annotations such as @PreAuthorize directly on methods to specify which roles are allowed to execute them.
To enable method-level security, add @EnableMethodSecurity to the security configuration. Once method-level security is enabled, we can use @PreAuthorize on controller or service methods.
Let's understand it with the help of given example below.
package com.javabytechie.bank.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class BankController {
// Anyone can call this, even without logging in
@GetMapping("/public/branches")
public List<String> branches() {
return List.of("Bengaluru", "Pune", "Delhi");
}
// Customers may see only their own account; managers and admins may see any account
@GetMapping("/customer/accounts/{username}")
@PreAuthorize("#username == authentication.name or hasAnyRole('MANAGER', 'ADMIN')")
public Map<String, Object> account(@PathVariable String username) {
return Map.of("owner", username, "balance", 25_000);
}
@PostMapping("/manager/loans/{loanId}/approve")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
public String approveLoan(@PathVariable Long loanId) {
return "Loan " + loanId + " approved";
}
@DeleteMapping("/admin/users/{username}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable String username) {
return ResponseEntity.noContent().build();
}
}
The account rule mixes RBAC with an ownership check. Being a customer is not enough: #username == authentication.name also makes sure Riya can open /accounts/riya but not /accounts/arjun.
Try it with curl:
# 200 OK: Riya opens her own account
curl -u riya:riya123 http://localhost:8080/api/customer/accounts/riya
# 403 Forbidden: Riya tries to open someone else's account
curl -u riya:riya123 http://localhost:8080/api/customer/accounts/arjun
# 200 OK: Arjun (manager) approves a loan
curl -u arjun:arjun123 -X POST http://localhost:8080/api/manager/loans/7/approve
# 401 Unauthorized: no login details sent at all
curl -X DELETE http://localhost:8080/api/admin/users/riya
Remember the difference: 401 Unauthorized means "we don't know who you are", while 403 Forbidden means "we know who you are, but your role does not allow this".
12. Role Hierarchy
Writing hasAnyRole('MANAGER', 'ADMIN') everywhere gets repetitive. A role hierarchy tells Spring that higher roles include lower ones, so you can simply write hasRole('MANAGER').
// Spring Security 6.3+ (Spring Boot 3.3+)
@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("MANAGER")
.role("MANAGER").implies("CUSTOMER")
.build();
}
With this bean in place, an ADMIN automatically passes every hasRole('MANAGER') and hasRole('CUSTOMER') check. Your rules become shorter and easier to read:
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasRole("MANAGER")
.requestMatchers("/api/customer/**").hasRole("CUSTOMER")
.anyRequest().authenticated()
)
13. Storing Roles in a Database
In real applications, users, roles and permissions live in a database so they can be changed without redeploying. A typical design uses three tables plus two join tables:
app_users ──< user_roles >── roles ──< role_permissions >── permissions
(id, username, (id, name) (id, name)
password, enabled)
Add spring-boot-starter-data-jpa and a database driver to your project, then map the tables to entities. Getters are left out below to keep the code short.
@Entity
@Table(name = "permissions")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name; // e.g. LOAN_APPROVE
// getters
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name; // e.g. MANAGER (stored without the ROLE_ prefix)
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
// getters
}
@Entity
@Table(name = "app_users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password; // BCrypt hash
private boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
// getters
}
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
}
package com.javabytechie.bank.security;
import com.javabytechie.bank.entity.AppUser;
import com.javabytechie.bank.entity.Permission;
import com.javabytechie.bank.entity.Role;
import com.javabytechie.bank.repository.AppUserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
private final AppUserRepository userRepository;
public DatabaseUserDetailsService(AppUserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
Set<GrantedAuthority> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
for (Permission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getName()));
}
}
return User.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.disabled(!user.isEnabled())
.build();
}
}
@Service
public class LoanService {
@PreAuthorize("hasAuthority('LOAN_APPROVE')")
public void approve(Long loanId) {
// approve the loan
}
@PreAuthorize("hasAuthority('REPORT_EXPORT')")
public byte[] exportMonthlyReport() {
// build and return the report
return new byte[0];
}
}
The DatabaseUserDetailsService tab is the UserDetailsService. It loads the user and turns each role into a ROLE_ authority and each permission into a plain authority. Spring Security uses this bean automatically, so remove the in-memory one from the earlier example.
Now you can protect actions by permission instead of by role. If you later move LOAN_APPROVE to another role, this code does not change at all. The LoanService tab shows an example.
14. Testing RBAC
Security rules are easy to break by accident, so test them. @WithMockUser runs a test as a fake logged-in user with the roles you choose.
package com.javabytechie.bank;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class BankControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void anonymousUserIsUnauthorized() throws Exception {
mockMvc.perform(get("/api/customer/accounts/riya"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "riya", roles = "CUSTOMER")
void customerCanSeeOwnAccount() throws Exception {
mockMvc.perform(get("/api/customer/accounts/riya"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "riya", roles = "CUSTOMER")
void customerCannotSeeOtherAccounts() throws Exception {
mockMvc.perform(get("/api/customer/accounts/arjun"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "MANAGER")
void managerCannotDeleteUsers() throws Exception {
mockMvc.perform(delete("/api/admin/users/riya"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminCanDeleteUsers() throws Exception {
mockMvc.perform(delete("/api/admin/users/riya"))
.andExpect(status().isNoContent());
}
}
15. RBAC in Microservices
In a microservices system, users usually log in through a central authorization server (such as Keycloak, Okta or Auth0). It issues a JWT access token that contains the user's roles. Each microservice reads the roles from the token and applies its own RBAC rules, without calling a user database.
A token payload might look like this:
{
"sub": "arjun",
"roles": ["MANAGER"],
"iss": "https://auth.example.com",
"exp": 1767225600
}
With spring-boot-starter-oauth2-resource-server, tell Spring Security to read the roles claim and add the ROLE_ prefix:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/manager/**").hasRole("MANAGER")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter rolesConverter = new JwtGrantedAuthoritiesConverter();
rolesConverter.setAuthoritiesClaimName("roles");
rolesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(rolesConverter);
return converter;
}
}
Point the service at your authorization server so it can verify token signatures:
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com
An API gateway can do a first, coarse check (for example, "only staff can reach /admin routes"), but each service should still enforce its own rules. Never rely on the gateway alone.
16. Advantages and Limitations
17.1 Advantages
- Easy to understand, explain and audit.
- Permissions are managed in one central place.
- New users are set up quickly by assigning a role.
- Supports the principle of least privilege.
- Well supported by frameworks such as Spring Security and by identity providers.
17.2 Limitations
- Role explosion: if you create a new role for every small difference (e.g. "Manager-Pune-Weekend"), the number of roles grows out of control.
- Not context-aware: plain RBAC cannot express rules like "only during office hours" or "only for amounts below a limit". You need extra checks or ABAC for those.
- Ownership rules need extra code: "users can edit only their own data" is not a role. It must be checked separately, as shown in section 11.
17. Best Practices
Follow these practices to keep your roles easy to manage and your application safe as it grows:
| Practice | Why it matters | Example |
|---|---|---|
| Follow least privilege | If an account is misused, the damage is limited to what that role can do. | Start every new role with no permissions and add only what the job needs. |
| Name roles after jobs, permissions after actions | Clear names make roles easy to understand and review. | Roles: ACCOUNTANT, SUPPORT_AGENT. Permissions: INVOICE_CREATE, TICKET_CLOSE. |
| Check permissions in code, not role names | Roles can be reorganized later without changing code. | hasAuthority("INVOICE_APPROVE") instead of hasRole("MANAGER"). |
| Deny by default | Any URL you forget to list is still protected. | End your URL rules with anyRequest().authenticated() or denyAll(). |
| Enforce rules on the server | Hiding a button in the UI does not stop someone calling the API directly. | Put @PreAuthorize on the service method behind the button. |
| Review role assignments regularly | People change jobs, and old access quietly piles up. | Run a regular access review, and remove roles when someone moves team or leaves. |
| Log access-denied events | Repeated 403 errors reveal misuse or a wrongly configured role. | Log the user, URL and time of every denied request. |
| Write tests for allowed and denied cases | A broken rule is caught before it reaches production. | For each important rule, one test expects 200 and one expects 403. |
The third practice is the one that saves the most work later. Compare these two versions of the same method:
// Avoid: the rule is tied to a role name
@PreAuthorize("hasRole('MANAGER')")
public void approveInvoice(Long invoiceId) { ... }
// Prefer: the rule is tied to the action
@PreAuthorize("hasAuthority('INVOICE_APPROVE')")
public void approveInvoice(Long invoiceId) { ... }
With the second version, you can later give INVOICE_APPROVE to a new role, such as FINANCE_LEAD, without touching the code.
18. Common Mistakes to Avoid
These mistakes are easy to make and often go unnoticed until someone gets access they should not have:
| Mistake | What goes wrong | How to fix it |
|---|---|---|
Adding the ROLE_ prefix twice | hasRole("ROLE_ADMIN") is rejected in recent Spring versions, because hasRole() adds the prefix itself. | Use hasRole("ADMIN") or hasAuthority("ROLE_ADMIN"). |
| Wrong rule order | Rules are checked from top to bottom. If /api/** comes before /api/admin/**, the admin rule is never reached. | Put specific rules first and general rules last. |
Forgetting @EnableMethodSecurity | Every @PreAuthorize annotation is silently ignored. | Add @EnableMethodSecurity to a configuration class. |
| Calling a secured method from the same class | Method security works through Spring proxies, so an internal this.approve() call skips the check. | Move the secured method to another bean, or secure the public method that is called from outside. |
| Giving everyone the ADMIN role | Added "just to make it work" during development, it is easily forgotten and reaches production. | Create proper test users for each role, and review role assignments before release. |
| Checking roles only in the frontend | Anyone can call your API directly with tools like curl. | Always enforce the same rules on the server. |
| Confusing 401 and 403 | Clients cannot tell whether to log in again or to stop trying. | Return 401 when the user is not logged in, and 403 when they are logged in but not allowed. |
The first two mistakes are the most common. Here is what they look like in code:
// Wrong: hasRole() adds "ROLE_" itself, so this looks for "ROLE_ROLE_ADMIN"
.requestMatchers("/admin/**").hasRole("ROLE_ADMIN")
// Right: either let hasRole() add the prefix...
.requestMatchers("/admin/**").hasRole("ADMIN")
// ...or use hasAuthority() with the full name
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
// Wrong: "/api/**" matches admin URLs first, so any logged-in user gets in
.requestMatchers("/api/**").authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Right: the specific rule comes first
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
19. Summary
Role Based Access Control gives permissions to roles and roles to users. This simple idea makes systems more secure, easier to manage and ready to grow.
In Spring Boot, you can apply RBAC at two levels: URL rules in a SecurityFilterChain, and method rules with @PreAuthorize. Add a role hierarchy to avoid repetition, store roles and permissions in a database for flexibility, and read roles from JWT tokens in microservices. Combine RBAC with ownership checks where needed, follow least privilege, and test both the "allowed" and "denied" paths.
Frequently Asked Questions (FAQ)
Q1. What is Role Based Access Control in simple words?
RBAC is a way to control access where permissions are given to roles, and roles are given to users. A user can do whatever their roles allow, and nothing else.
Q2. What is the difference between authentication and authorization?
Authentication checks who you are, for example by verifying your password. Authorization checks what you may do after you are logged in.
RBAC is an authorization model, so it always runs after authentication.
Q3. What is the difference between a role and a permission?
A permission is one specific action, such as LOAN_APPROVE. A role is a named group of permissions that matches a job, such as MANAGER.
Users receive roles, and through those roles they receive permissions.
Q4. Can one user have more than one role?
Yes. A user gets the combined permissions of all their roles. For example, a team lead could have both EMPLOYEE and APPROVER.
In Spring Security, simply add several roles:
User.withUsername("meera")
.password(encoder.encode("meera123"))
.roles("EMPLOYEE", "APPROVER")
.build();
Q5. What is the principle of least privilege?
It means every user and role should have only the minimum access needed to do their job. If an account is ever misused or stolen, the damage stays small.
Q6. What is separation of duties?
It means splitting a sensitive task between different roles so that one person cannot finish it alone. For example, one role creates a payment, and a different role approves it. This helps prevent fraud and costly mistakes.
Q7. What is a role hierarchy?
A role hierarchy lets higher roles inherit the permissions of lower roles. If ADMIN is above MANAGER, every admin can automatically do everything a manager can.
In Spring Security 6.3+, you define it like this:
@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("MANAGER")
.role("MANAGER").implies("CUSTOMER")
.build();
}
Q8. What is the difference between hasRole() and hasAuthority() in Spring Security?
hasRole("ADMIN") automatically adds the ROLE_ prefix and checks for ROLE_ADMIN.
hasAuthority("ADMIN") checks for the exact text ADMIN, with no prefix. Use hasAuthority for fine-grained permissions such as LOAN_APPROVE.
Q9. Why does Spring Security add the ROLE_ prefix?
Spring keeps roles and permissions in the same list of authorities. The ROLE_ prefix is how it tells them apart. This lets you write short checks like hasRole("ADMIN") while still storing permissions such as REPORT_EXPORT in the same list.
Q10. Why is my @PreAuthorize annotation not working?
Check these common causes:
@EnableMethodSecurityis missing from your configuration.- The method is called from inside the same class, so the Spring proxy is skipped.
- The class is not a Spring bean (for example, it was created with
new). - The role name includes the prefix twice, such as
hasRole('ROLE_ADMIN').
Q11. What is the difference between HTTP 401 and 403?
401 Unauthorized means the request has no valid login, so the server does not know who you are.
403 Forbidden means the server knows who you are, but your roles do not allow this action. RBAC failures return 403.
Q12. Should I secure URLs or methods?
Use both. URL rules give a broad first layer (for example, the whole /api/admin/** area). Method rules with @PreAuthorize protect specific business actions, even if they are called from somewhere unexpected, such as a scheduled job or another controller.
Q13. How do I let users access only their own data?
Combine a role check with an ownership check in @PreAuthorize:
@GetMapping("/customer/accounts/{username}")
@PreAuthorize("#username == authentication.name or hasRole('MANAGER')")
public Map<String, Object> account(@PathVariable String username) {
return Map.of("owner", username);
}
A customer passes only when the requested username is their own. Managers can view any account.
Q14. How do I get the current user's roles in Java code?
Read them from the Authentication object. In a controller, Spring can pass it to you as a parameter:
@GetMapping("/api/me")
public Map<String, Object> me(Authentication authentication) {
List<String> authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
return Map.of("user", authentication.getName(), "authorities", authorities);
}
Q15. Where should roles and permissions be stored?
For learning and tests, in-memory users are fine. For real applications, store them in a database (users, roles, permissions and join tables) or in an identity provider such as Keycloak, Okta or Microsoft Entra ID. Then they can be changed without redeploying the application.
Q16. How does RBAC work in microservices?
A central authorization server puts the user's roles into a JWT access token. Every service checks the token's signature, reads the roles from it, and applies its own rules. No service needs to call a shared user database for each request.
See section 15 for the Spring configuration.
Q17. Is hiding buttons in the user interface enough for RBAC?
No. Hiding buttons only improves the user experience. Anyone can call your API directly with tools like curl or Postman. Every rule must also be enforced on the server.
Q18. What is the difference between RBAC and ABAC?
RBAC decides access using only the user's roles: "managers may approve loans".
ABAC (Attribute Based Access Control) uses many attributes, such as department, amount, location or time: "managers may approve loans below a limit in their own branch". ABAC is more flexible but harder to manage. Many systems use RBAC first and add a few attribute checks where needed.
Q19. What is role explosion, and how can I avoid it?
Role explosion happens when teams create a new role for every small variation, until there are hundreds of roles that nobody understands.
To avoid it:
- Keep roles tied to real job functions.
- Use fine-grained permissions inside roles instead of creating new roles.
- Handle special conditions (branch, amount, ownership) with extra checks, not extra roles.
- Review and remove unused roles regularly.
Q20. How do I test RBAC rules in Spring Boot?
Add spring-security-test and use @WithMockUser to run each test as a user with specific roles. Test both the allowed and the denied case:
@Test
@WithMockUser(roles = "MANAGER")
void managerCannotDeleteUsers() throws Exception {
mockMvc.perform(delete("/api/admin/users/riya"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminCanDeleteUsers() throws Exception {
mockMvc.perform(delete("/api/admin/users/riya"))
.andExpect(status().isNoContent());
}