Skip to content

Spring Boot Security (Part 9) | Method Security and Role Based Authorization

The Department Key and Safe Deposit Box Analogy

Imagine a high security financial institution. At the front entrance, security guards verify that you work for the bank. That is URL level security: you are permitted to walk through the front doors.

However, once you are inside the building, you cannot simply open every safe, file cabinet, or executive desk drawer. The vault requires a combination known only to the vault supervisor. The payroll spreadsheet can only be modified by the HR director. And an account balance record can only be altered by the specific customer service representative assigned to that account ID.

In software architecture, URL level security (in SecurityFilterChain) is that front door security check. But URL security is coarse: it only looks at path strings like /api/orders/**. Method Security is the safe deposit box lock: it secures individual business methods inside the @Service layer. Using annotations like @PreAuthorize, you can enforce fine grained authorization rules using Spring Expression Language (SpEL), verifying not just whether the user is an admin, but whether the authenticated user is the actual owner of the specific order being deleted.

This lecture covers enabling method security in Spring Boot 3 (@EnableMethodSecurity), role based authorization, SpEL expressions, @PreAuthorize versus @PostAuthorize, and @Secured.


Enabling Method Security in Spring Boot 3

Method security is disabled by default. In Spring Boot 3 (Spring Security 6), activate it using @EnableMethodSecurity:

java
package com.example.orderservice.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity(
    prePostEnabled = true, // Enables @PreAuthorize and @PostAuthorize (default is true)
    securedEnabled = true, // Enables legacy @Secured
    jsr250Enabled = true   // Enables standard @RolesAllowed
)
public class MethodSecurityConfig {
}

Notice: In Spring Boot 3, @EnableMethodSecurity replaced the older @EnableGlobalMethodSecurity.


The Core Method Security Annotations

Spring Security provides three main annotations for securing methods:

AnnotationExpression SupportEvaluation TimingTypical Use Case
@PreAuthorizeFull SpEL SupportEvaluated before method executesFine grained checks, parameter validation, role checks
@PostAuthorizeFull SpEL SupportEvaluated after method executesInspecting the returned object before returning to caller
@SecuredString literal role list (No SpEL)Evaluated before method executesSimple role checks (legacy projects)

@PreAuthorize: The Most Powerful Security Annotation

@PreAuthorize evaluates a Spring Expression Language (SpEL) expression before the method executes. If the expression evaluates to false, Spring throws an AccessDeniedException (yielding an HTTP 403 Forbidden response).

1. Basic Role and Authority Checks

java
@Service
public class OrderService {

    // Requires ROLE_ADMIN
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(Long orderId) {
        orderRepository.deleteById(orderId);
    }

    // Requires either ADMIN or MANAGER
    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
    public void approveRefund(Long orderId) { /* ... */ }

    // Checks specific granular authority
    @PreAuthorize("hasAuthority('ORDER_EXPORT')")
    public byte[] exportAuditReport() { /* ... */ }
}

2. Inspecting Method Arguments

You can reference method arguments using #parameterName:

java
// User can only view profile if they are an ADMIN OR if the username matches their own account!
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public UserProfile getProfile(String username) {
    return profileRepository.findByUsername(username);
}

3. Calling Custom Security Evaluation Beans

For complex business logic that cannot fit into a single SpEL line, delegate to a custom Spring bean:

java
@PreAuthorize("@securityEvaluator.canAccessOrder(#orderId)")
public Order getOrderDetails(Long orderId) {
    return orderRepository.findById(orderId).orElseThrow();
}

The custom evaluator bean:

java
package com.example.orderservice.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component("securityEvaluator")
public class SecurityEvaluator {

    public boolean canAccessOrder(Long orderId) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUsername = auth.getName();

        // Check database or domain logic
        return orderService.isOrderOwnedByUser(orderId, currentUsername);
    }
}

@PostAuthorize: Inspecting Return Values

Sometimes you cannot know whether a user is authorized until the data is actually loaded from the database.

For example, when retrieving a private document, the method must execute first so Spring can check whether returnObject.getOwnerUsername() matches the caller:

java
@Service
public class DocumentService {

    // Executes method first, then validates returnObject before handing it to caller!
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public Document getDocumentById(Long docId) {
        System.out.println("Executing database read for document: " + docId);
        return documentRepository.findById(docId).orElseThrow();
    }
}

If the document owner is "bob", but "alice" called the method, Spring throws AccessDeniedException after the method finishes, preventing "alice" from seeing the document!

(Warning: Never use @PostAuthorize on methods that perform mutations or database writes, because the database operation will have already committed before the authorization check executes!).


Filtering Collections: @PreFilter and @PostFilter

Spring Security can filter collections passed into or returned from methods:

java
// Filters the incoming list: removes any order that does not belong to the user
@PreFilter("filterObject.owner == authentication.name")
public void processBatchOrders(List<Order> orders) {
    for (Order order : orders) {
        // Only orders belonging to the authenticated user remain in the list!
        orderRepository.save(order);
    }
}

// Filters the outgoing return collection
@PostFilter("filterObject.owner == authentication.name or hasRole('ADMIN')")
public List<Order> getAllOrders() {
    return orderRepository.findAll();
}

In @PreFilter and @PostFilter, the special variable filterObject represents each individual element in the collection.


Interview Questions & Pitfalls

Q1: Which annotation activates method security in Spring Boot 3?

In Spring Boot 3, method security is activated using @EnableMethodSecurity placed on a @Configuration class. This replaces the deprecated @EnableGlobalMethodSecurity from Spring Boot 2.

Q2: What is the difference between @PreAuthorize and @PostAuthorize?

@PreAuthorize evaluates authorization rules before the method executes; if the check fails, the method never runs. @PostAuthorize allows the method to execute, and evaluates the security expression after execution, granting access to returnObject to decide whether the caller is allowed to receive the result.

Q3: Why should @PostAuthorize never be used on methods that modify database state?

@PostAuthorize executes after the method body finishes. If the method executes database updates or deletes, those mutations have already occurred and potentially committed before the security check runs. Even though an AccessDeniedException is thrown to the caller, the unauthorized state mutation has already taken place.

Q4: How does @PreAuthorize inspect method parameters?

You reference method parameters in SpEL using the hash symbol followed by the parameter name (e.g. #userId, #order.customerId). Spring maps the parameter names using reflection or compiled parameter metadata.

Q5: What exception is thrown when a method security check fails, and how does Spring Boot handle it?

When a method security check fails, Spring Security throws org.springframework.security.access.AccessDeniedException. In web controllers, this is intercepted by Spring Security's exception translation filter or a global @RestControllerAdvice, returning an HTTP 403 Forbidden status code to the client.