Spring Security: Method-Level Security: @PreAuthorize & SpEL

Method-level security lets you enforce authorization rules directly on service and repository methods using annotations, independently of HTTP-layer rules. @PreAuthorize and @PostAuthorize use Spring Expression Language (SpEL) to inspect the authenticated principal, arguments, and return values.

Contents

Enabling Method Security

Add @EnableMethodSecurity to any @Configuration class. This replaces the older @EnableGlobalMethodSecurity (deprecated in Spring Security 6). It activates pre/post annotations by default.

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

@Configuration
@EnableMethodSecurity          // enables @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter
public class MethodSecurityConfig {
    // No additional beans needed — method security is auto-configured
}
@EnableMethodSecurity uses Spring AOP proxies to intercept method calls. Both securedEnabled = true (for the legacy @Secured) and jsr250Enabled = true (for @RolesAllowed) can be set as attributes if you need those older annotations too.

@PreAuthorize: Guard Before Execution

@PreAuthorize runs before the method body. If the SpEL expression evaluates to false, Spring throws AccessDeniedException and the method never executes.

Java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    // Only users with ROLE_ADMIN may call this
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(Long orderId) {
        orderRepository.deleteById(orderId);
    }

    // Either ADMIN or USER with the "order:read" authority
    @PreAuthorize("hasRole('ADMIN') or hasAuthority('order:read')")
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId).orElseThrow();
    }

    // Method argument available as #customerId in SpEL
    @PreAuthorize("hasRole('ADMIN') or #customerId == authentication.principal.id")
    public List<Order> getOrdersForCustomer(Long customerId) {
        return orderRepository.findByCustomerId(customerId);
    }

    // Only authenticated users (any role)
    @PreAuthorize("isAuthenticated()")
    public List<Order> getMyOrders() {
        return orderRepository.findByCurrentUser();
    }
}

@PostAuthorize: Guard on Return Value

@PostAuthorize runs after the method returns but before the result is handed back to the caller. Use it to enforce ownership: the method runs first and the result is available as returnObject in SpEL.

Java
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

    // Allow the call but throw if the returned document doesn't belong to the caller
    @PostAuthorize("returnObject.ownerUsername == authentication.name")
    public Document findDocument(Long id) {
        return documentRepository.findById(id).orElseThrow();
    }

    // Admin can see any document; others only their own
    @PostAuthorize("hasRole('ADMIN') or returnObject.ownerUsername == authentication.name")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElseThrow();
    }
}
Because @PostAuthorize runs after the method body, any side effects (writes, external calls) already happened when the check fails. Use it only for read operations where the result ownership needs to be validated.

SpEL Reference: Principal, Args & Built-ins

Spring Security exposes a rich set of built-in SpEL functions and objects inside method security expressions:

ExpressionDescription
authenticationThe current Authentication object.
authentication.nameThe authenticated username.
authentication.principalThe UserDetails (or custom principal) object.
authentication.principal.idAny field on your custom principal (e.g., user ID).
hasRole('ADMIN')True if the user has ROLE_ADMIN (prefix added automatically).
hasAuthority('order:write')True if the user has the exact authority string.
hasAnyRole('ADMIN', 'MANAGER')True if the user has any of the listed roles.
isAuthenticated()True for any logged-in user (not anonymous).
isAnonymous()True for unauthenticated (anonymous) users.
#paramNameMethod parameter by name: requires -parameters compiler flag or @Param.
returnObjectThe method return value (only in @PostAuthorize/@PostFilter).
hasPermission(obj, 'read')Delegates to a custom PermissionEvaluator bean.
Java
// Accessing a field on a custom UserDetails implementation
@PreAuthorize("authentication.principal.tenantId == #tenantId")
public List<Report> getReports(Long tenantId) { ... }

// Checking a nested property of a method argument (uses SpEL property access)
@PreAuthorize("hasRole('ADMIN') or #order.customerId == authentication.principal.id")
public Receipt checkout(Order order) { ... }

@PreFilter & @PostFilter: Collection Filtering

@PreFilter removes elements from a collection argument before the method runs. @PostFilter removes elements from a collection return value. The current element being evaluated is filterObject.

Java
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.stereotype.Service;

@Service
public class FileService {

    // Strip out any files the caller doesn't own before saving
    @PreFilter("filterObject.ownerUsername == authentication.name")
    public void saveFiles(List<FileEntry> files) {
        fileRepository.saveAll(files);  // only files owned by caller reach here
    }

    // Return only files belonging to the caller (admin sees all)
    @PostFilter("hasRole('ADMIN') or filterObject.ownerUsername == authentication.name")
    public List<FileEntry> listFiles() {
        return fileRepository.findAll();
    }
}
@PostFilter loads the entire collection from the database and then filters in memory. For large collections this is expensive: prefer adding ownership predicates to your repository query instead.

Custom Permission Evaluator

When simple role/authority checks aren't enough, implement PermissionEvaluator to express domain-specific authorization logic. Then call it in SpEL with hasPermission().

Java
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;

@Component
public class OrderPermissionEvaluator implements PermissionEvaluator {

    private final OrderRepository orderRepository;

    public OrderPermissionEvaluator(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
        if (targetDomainObject instanceof Order order) {
            return switch (permission.toString()) {
                case "read"   -> order.getCustomerId().equals(getPrincipalId(auth));
                case "delete" -> auth.getAuthorities().stream()
                                    .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
                default -> false;
            };
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
        if ("Order".equals(targetType)) {
            Order order = orderRepository.findById((Long) targetId).orElseThrow();
            return hasPermission(auth, order, permission);
        }
        return false;
    }

    private Long getPrincipalId(Authentication auth) {
        return ((CustomUserDetails) auth.getPrincipal()).getId();
    }
}
Java
// Register the evaluator in a MethodSecurityExpressionHandler bean
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
        OrderPermissionEvaluator evaluator) {
    DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
    handler.setPermissionEvaluator(evaluator);
    return handler;
}

// Use it in annotations:
@PreAuthorize("hasPermission(#orderId, 'Order', 'read')")
public Order getOrder(Long orderId) { ... }

@PreAuthorize("hasPermission(#order, 'delete')")
public void cancelOrder(Order order) { ... }

Testing Method Security

Use @WithMockUser to simulate an authenticated user in unit tests. @WithMockUser(roles = "ADMIN") sets up a principal with ROLE_ADMIN. Import spring-security-test for these annotations.

XML
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <scope>test</scope>
</dependency>
Java
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

@SpringBootTest
class OrderServiceSecurityTest {

    @Autowired OrderService orderService;

    @Test
    @WithMockUser(roles = "ADMIN")
    void adminCanDeleteOrder() {
        assertThatNoException().isThrownBy(() -> orderService.deleteOrder(1L));
    }

    @Test
    @WithMockUser(roles = "USER")
    void userCannotDeleteOrder() {
        assertThatThrownBy(() -> orderService.deleteOrder(1L))
            .isInstanceOf(org.springframework.security.access.AccessDeniedException.class);
    }

    @Test
    @WithMockUser(username = "alice", roles = "USER")
    void userCanReadOwnOrders() {
        // #customerId == authentication.principal.id — would need custom @WithMockUser
        // Use @WithUserDetails for custom UserDetails principal
    }
}