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 is an authorization model. Authorization happens after authentication:
- Authentication checks who you are (for example, with a username and password).
- Authorization checks what you are allowed to do once you are logged in.
In RBAC, the answer to the second question comes from the user's roles. A role groups together all the permissions needed for a job, such as "Manager" or "Support Agent". When a person joins, changes jobs or leaves, you only change which roles they have. The permissions follow automatically.
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. Real-Life Analogy
Think of a hospital. Everyone works in the same building, but access depends on the job:
- Doctors can read patient reports and prescribe medicine.
- Nurses can record vital signs and give medicine that has been prescribed.
- Receptionists can book appointments but cannot open medical records.
When a new nurse joins, the hospital does not write a new list of rules for that person. It simply gives them the "Nurse" badge. That badge is the role, and it opens exactly the right doors. Software RBAC works the same way.
7. 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.
8. 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".
9. 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).
10. 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>
11. URL-Based RBAC
The first layer of protection is to secure URL patterns. Each area of the API is linked to the roles that may use it. For learning, we keep three sample users in memory.
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.
12. Method-Level RBAC
URL rules protect whole areas. For finer control, put rules directly on methods with @PreAuthorize. This works because we added @EnableMethodSecurity to the configuration.
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".
13. 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()
)
14. 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);
}
Next, create a 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.
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();
}
}
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:
@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];
}
}
15. 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());
}
}
16. 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.
17. 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 12.
18. Best Practices
- Follow least privilege. Start every role with no permissions and add only what is needed.
- Name roles after jobs (ACCOUNTANT, SUPPORT_AGENT), and permissions after actions (INVOICE_CREATE, TICKET_CLOSE).
- Check permissions in code, not role names, for specific actions. Roles can then be reorganized without code changes.
- Deny by default. End your URL rules with
anyRequest().authenticated()ordenyAll(). - Enforce rules on the server. Hiding a button in the UI is not security.
- Review role assignments regularly and remove access when people change jobs or leave.
- Log access-denied events so you can spot misuse and misconfigured roles.
- Write tests for both allowed and denied cases of every important rule.
19. Common Mistakes to Avoid
- Adding the prefix twice.
hasRole("ROLE_ADMIN")is wrong in recent Spring versions. UsehasRole("ADMIN")orhasAuthority("ROLE_ADMIN"). - Wrong rule order. Placing
/api/**before/api/admin/**means the admin rule is never reached. - Forgetting
@EnableMethodSecurity. Without it, every@PreAuthorizeannotation is silently ignored. - Calling a secured method from the same class. Method security works through Spring proxies, so an internal
this.approve()call skips the check. - Giving everyone the ADMIN role "just to make it work" during development and forgetting to remove it.
- Checking roles only in the frontend. Anyone can call your API directly with tools like
curl. - Confusing 401 and 403. 401 means not logged in; 403 means logged in but not allowed.
20. 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 16 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());
}