Full Stack • Java • System Design • Cloud • AI Engineering

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 for ROLE_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