Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering 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
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,051visitors

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.

Advertisement

Common RBAC mistakes, explained

The configuration above is straightforward to write. The bugs usually come from a few recurring misunderstandings:

  • The ROLE_ prefix mismatch. hasRole("ADMIN") checks for the authority ROLE_ADMIN. hasAuthority("ADMIN") checks for exactly ADMIN. If your UserDetails returns ADMIN without the prefix, hasRole("ADMIN") silently denies everyone. Pick one convention: roles as ROLE_* authorities, and fine-grained permissions (invoice:write) as plain authorities checked with hasAuthority.
  • Method security not enabled, or bypassed. @PreAuthorize does nothing unless @EnableMethodSecurity is present. It also only works through Spring's proxy, so a call from one method to another in the same class skips the check entirely. Put secured methods on beans called from outside, or secure the entry point.
  • Securing URLs but not objects. hasRole('USER') on /api/orders/** lets any user read any order, for example by changing the ID in the URL. Role checks answer "what kind of action?". You still need ownership checks for "on whose data?", in @PreAuthorize("@orderSecurity.isOwner(#id, authentication)"), a PermissionEvaluator, or the query itself (findByIdAndOwnerId).
  • Role explosion. Creating a new role for every combination of needs (ROLE_EU_SUPPORT_READONLY_BILLING) becomes unmanageable. Keep a small set of roles, map them to permissions, and check permissions in code. Changing what a role can do then becomes a data change, not a code change.
  • Relying only on hidden UI. Hiding the "Delete" button isn't authorization. Every server endpoint must enforce its own checks, because clients can call APIs directly.

Role hierarchies

If admins should automatically have every manager permission, and managers every user permission, declare a hierarchy instead of listing roles repeatedly:

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

With method security, expose the hierarchy to the expression handler (Spring Security 6 picks up a RoleHierarchy bean for @PreAuthorize), so that hasRole('USER') also passes for managers and admins.

Testing authorization rules

Authorization is security-critical, and easy to break during refactoring, so test it explicitly:

@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderSecurityTest {
    @Autowired MockMvc mvc;

    @Test @WithMockUser(roles = "USER")
    void userCannotDeleteOrders() throws Exception {
        mvc.perform(delete("/api/orders/42").with(csrf())).andExpect(status().isForbidden());
    }

    @Test @WithMockUser(roles = "ADMIN")
    void adminCanDeleteOrders() throws Exception {
        mvc.perform(delete("/api/orders/42").with(csrf())).andExpect(status().isNoContent());
    }

    @Test
    void anonymousIsRejected() throws Exception {
        mvc.perform(get("/api/orders/42")).andExpect(status().isUnauthorized());
    }
}

Test the denials as carefully as the permissions. A test suite that only checks that admins can do things never notices when a change accidentally lets everyone do them.

Follow-up questions this topic invites — and their answers

Q: What's the difference between hasRole and hasAuthority? A: hasRole("X") checks for the authority ROLE_X, adding the prefix automatically. hasAuthority("X") checks for the exact string X. They refer to the same list of granted authorities. Roles are simply authorities that follow the ROLE_ naming convention.

Q: Why doesn't my @PreAuthorize annotation work? A: Common causes: method security isn't enabled (@EnableMethodSecurity), the method is called from inside the same class (bypassing the security proxy), the method isn't public, or the bean isn't managed by Spring. Also check the ROLE_ prefix expectations in the expression.

Q: RBAC or ABAC: which should I use? A: RBAC (role-based) is simple, and fits most applications when combined with ownership checks. ABAC (attribute-based) evaluates attributes of the user, the resource and the context (department, classification, time, location). It's worth it when rules depend on many such attributes. Many systems use RBAC for coarse access, and a few attribute-based checks for sensitive operations.

Q: Where should authorization checks live: controller, service or repository? A: Put coarse checks at the entry points (URL rules or controller methods), and object-level checks where the object is loaded, usually the service layer, or the repository query itself. Checks near the data are harder to bypass through a new endpoint that someone forgets to secure.

Q: How do you give users different permissions in different organizations or projects? A: Store role assignments scoped to a resource: (user, organization, role) rather than (user, role). Checks then ask "does this user have permission P in organization O?". Spring Security's PermissionEvaluator, or a custom policy bean, can implement that lookup.

Advertisement

Previous

Authentication Mechanics

Next

Password Encoding

AI Tutor

Lesson: Role-Based Access Control

Quick actions

AI responses can be inaccurate. Verify critical information.