Contents

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

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 runs before the method body. If the SpEL expression evaluates to false, Spring throws AccessDeniedException and the method never executes.

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 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.

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.

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.
// 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 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.

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.

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

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(); } } // 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) { ... }

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.

<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> 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 } }