Role-Based Access Control (RBAC)

1 minute read

RBAC restricts access based on user roles — admins can do everything, regular users have limited access.


Enable Method Security

@Configuration
@EnableMethodSecurity
public class SecurityConfig { }

@PreAuthorize — SpEL Expressions

@RestController
@RequestMapping("/api/books")
public class BookController {

    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        bookService.delete(id);
        return ResponseEntity.noContent().build();
    }

    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
    @PostMapping
    public ResponseEntity<BookResponse> create(@Valid @RequestBody CreateBookRequest request) {
        return ResponseEntity.created(URI.create("/api/books/1"))
            .body(bookService.create(request));
    }

    @PreAuthorize("hasAuthority('BOOK_READ')")
    @GetMapping("/{id}")
    public BookResponse getById(@PathVariable Long id) {
        return bookService.findById(id);
    }

    // Access current user
    @PreAuthorize("#username == authentication.principal.username or hasRole('ADMIN')")
    @GetMapping("/user/{username}")
    public List<BookResponse> getByUser(@PathVariable String username) {
        return bookService.findByUser(username);
    }
}

@Secured and @RolesAllowed

@Secured("ROLE_ADMIN")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) { }

@RolesAllowed({"ADMIN", "MANAGER"})
@PostMapping
public BookResponse create(@RequestBody CreateBookRequest request) { }
Annotation SpEL Support Prefix Required
@PreAuthorize ✅ No (hasRole('ADMIN'))
@Secured ❌ Yes (ROLE_ADMIN)
@RolesAllowed ❌ No

Use @PreAuthorize — most flexible.


Roles vs Authorities

// Roles = coarse-grained
UserDetails admin = User.builder()
    .username("admin")
    .password(encoder.encode("pass"))
    .roles("ADMIN")                        // stored as ROLE_ADMIN
    .build();

// Authorities = fine-grained permissions
UserDetails editor = User.builder()
    .username("editor")
    .password(encoder.encode("pass"))
    .authorities("BOOK_READ", "BOOK_WRITE", "BOOK_DELETE")
    .build();
@PreAuthorize("hasRole('ADMIN')")          // checks ROLE_ADMIN
@PreAuthorize("hasAuthority('BOOK_WRITE')")  // checks BOOK_WRITE

Service-Level Security

@Service
public class BookService {

    @PreAuthorize("hasAuthority('BOOK_WRITE')")
    public BookResponse create(CreateBookRequest request) { ... }

    @PreAuthorize("hasAuthority('BOOK_DELETE') or hasRole('ADMIN')")
    public void delete(Long id) { ... }

    @PostAuthorize("returnObject.author() == authentication.principal.username or hasRole('ADMIN')")
    public BookResponse findById(Long id) { ... }
}

@PostAuthorize checks after the method executes — useful for filtering based on the return value.


Accessing Current User

@GetMapping("/me")
public UserResponse getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
    return userService.findByUsername(userDetails.getUsername());
}

// Or via SecurityContextHolder
public String getCurrentUsername() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    return auth.getName();
}

Role Hierarchy

@Bean
public RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy("""
        ROLE_ADMIN > ROLE_MANAGER
        ROLE_MANAGER > ROLE_USER
        ROLE_USER > ROLE_GUEST
    """);
}

Now ADMIN automatically has all MANAGER, USER, and GUEST permissions.


Key Takeaways

  1. @EnableMethodSecurity + @PreAuthorize = method-level security.
  2. Roles are coarse-grained (ADMIN, USER); authorities are fine-grained (BOOK_WRITE).
  3. @PreAuthorize supports SpEL — the most flexible option.
  4. Use @AuthenticationPrincipal to access the current user.
  5. Role hierarchy simplifies permissions — ADMIN inherits all lower roles.

What’s Next?

Next: Spring Boot Actuator — health checks, metrics, and monitoring.

Updated: