Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Spring Security & JWT Auth

Spring Security Basics

  • Spring Security Overview
  • JWT Authentication
  • Authentication Mechanics

Authorization

  • Role-Based Access Control
  • Password Encoding
  • OAuth2 & Social Login Basics
Chaturmind
← Spring Security & JWT Auth

Spring Security Basics

  • Spring Security Overview
  • JWT Authentication
  • Authentication Mechanics

Authorization

  • Role-Based Access Control
  • Password Encoding
  • OAuth2 & Social Login Basics
HomeLearnSpring BootSpring Security & JWT AuthAuthorization
✓ FreeIntermediate· 10 min read

Role-Based Access Control

@PreAuthorize, @Secured, method security — control access at the method level.

Published September 21, 2026


Role-Based Access Control (RBAC) in Spring Security

RBAC restricts access based on roles assigned to users. Spring Security supports two levels: URL-based (in SecurityFilterChain) and method-level (@PreAuthorize).

Roles vs Authorities

In Spring Security:

  • A role is a string prefixed with ROLE_ (e.g., ROLE_ADMIN)
  • An authority is a fine-grained permission (e.g., user:write, report:read)
// hasRole("ADMIN")   checks for GrantedAuthority = "ROLE_ADMIN"
// hasAuthority("user:write") checks for GrantedAuthority = "user:write" exactly

@PreAuthorize / @PostAuthorize — method-level security expressions

@PreAuthorize("hasRole('ADMIN')")
void deleteUser(String userId) { ... } // evaluated BEFORE the method runs

@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
void updateProfile(String userId, ProfileUpdate update) { ... } // can reference method arguments directly

@PostAuthorize("returnObject.ownerId == authentication.principal.id")
Document getDocument(String docId) { ... } // evaluated AFTER the method runs, can inspect the return value

@PreAuthorize runs its SpEL (Spring Expression Language) check before the method body executes, and can reference method parameters directly (#userId) — the standard choice for most authorization checks, since it prevents the method from running at all if the check fails. @PostAuthorize runs after, with access to returnObject — necessary specifically when the authorization decision depends on data only available in the result (e.g. "can this user see this document" when "whose document it is" is itself part of what the method fetches).

@Secured — a simpler alternative

@Secured("ROLE_ADMIN") void deleteUser(String userId) { ... }

Predates @PreAuthorize/@PostAuthorize, supports only simple role checks (no SpEL expressions, no access to method arguments or return values) — simpler to read for the common "just check a role" case, but far less flexible. Most modern Spring Security code defaults to @PreAuthorize even for simple role checks, reserving @Secured mainly for legacy codebases already using it consistently.

Enabling method security

@Configuration
@EnableMethodSecurity // required — these annotations do nothing without it
class SecurityConfig { ... }

None of @PreAuthorize/@PostAuthorize/@Secured have any effect until method security is explicitly enabled at the configuration level — a common "why isn't my @PreAuthorize doing anything" debugging dead-end is simply a missing @EnableMethodSecurity.

Custom permission evaluators — fine-grained, attribute-based access control

@PreAuthorize("hasPermission(#document, 'EDIT')")
void editDocument(Document document) { ... }

class DocumentPermissionEvaluator implements PermissionEvaluator {
    public boolean hasPermission(Authentication auth, Object target, Object permission) {
        Document doc = (Document) target;
        User user = (User) auth.getPrincipal();
        return doc.getOwnerId().equals(user.getId()) || user.hasRole("ADMIN");
        // arbitrarily complex logic — not limited to a simple role check
    }
}

When authorization depends on a relationship between the specific resource and the specific user ("can this user edit this document," not just "does this user have an EDIT role globally") rather than a simple role check, a custom PermissionEvaluator plugged into the hasPermission() SpEL function is the standard extension point — this is the mechanism behind genuinely fine-grained, per-resource authorization rather than coarse role gates.

Defining Roles in the User Entity

@Document("users")
public class User implements UserDetails {
    private String email;
    private String passwordHash;
    private Set<String> roles = new HashSet<>(); // e.g., ["USER", "ADMIN"]

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .toList();
    }

    @Override
    public String getUsername() { return email; }

    @Override
    public String getPassword() { return passwordHash; }

    // Other UserDetails methods default to true for active accounts
}

URL-Level Authorization

http.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.GET, "/api/v1/courses/**").permitAll()
    .requestMatchers(HttpMethod.GET, "/api/v1/lessons/**").authenticated()
    .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/v1/instructor/**").hasAnyRole("ADMIN", "INSTRUCTOR")
    .anyRequest().authenticated()
);

Method-Level Security

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {}

@RestController
public class CourseController {

    @GetMapping("/api/v1/courses")
    public List<CourseDto> listCourses() {
        return courseService.list(); // public — no annotation needed
    }

    @PostMapping("/api/v1/courses")
    @PreAuthorize("hasRole('ADMIN')")
    public CourseDto create(@RequestBody @Valid CourseRequest req) {
        return courseService.create(req);
    }

    @DeleteMapping("/api/v1/courses/{id}")
    @PreAuthorize("hasRole('ADMIN') or @courseService.isOwner(#id, authentication.name)")
    public void delete(@PathVariable String id) {
        courseService.delete(id);
    }
}

Custom Permission Evaluator

For complex rules, implement PermissionEvaluator:

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication auth, Object target, Object permission) {
        if (target instanceof String resourceId && permission instanceof String action) {
            // Check if user has permission to perform action on resource
            String userId = auth.getName();
            return resourceAccessService.canAccess(userId, resourceId, action);
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication auth, Serializable targetId,
            String targetType, Object permission) {
        return false;
    }
}

// Usage:
@PreAuthorize("hasPermission(#lessonId, 'read')")
public LessonDto getLesson(String lessonId) { ... }

Accessing Current User in Service Layer

@Service
public class OrderService {

    public List<Order> getMyOrders() {
        String username = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        return orderRepository.findByUserId(username);
    }
}

// Or inject via @AuthenticationPrincipal in controllers:
@GetMapping("/orders")
public List<OrderDto> getOrders(@AuthenticationPrincipal UserDetails user) {
    return orderService.getOrdersFor(user.getUsername());
}

Interview Tips

  1. The difference between hasRole('ADMIN') and hasAuthority('ROLE_ADMIN') — they're equivalent but hasRole auto-prepends ROLE_.
  2. @PreAuthorize vs @Secured: @PreAuthorize supports SpEL and is more flexible; @Secured only supports role names.
  3. Always use method-level security on the service layer if the same service is called from multiple controllers or background jobs.

Previous

Authentication Mechanics

Next

Password Encoding

AI Tutor

Lesson: Role-Based Access Control

Quick actions

AI responses can be inaccurate. Verify critical information.