Contents
- Enabling Method Security
- @PreAuthorize — Guard Before Execution
- @PostAuthorize — Guard on Return Value
- SpEL Reference: Principal, Args & Built-ins
- @PreFilter & @PostFilter — Collection Filtering
- Custom Permission Evaluator
- Testing 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.
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:
| Expression | Description |
| authentication | The current Authentication object. |
| authentication.name | The authenticated username. |
| authentication.principal | The UserDetails (or custom principal) object. |
| authentication.principal.id | Any 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. |
| #paramName | Method parameter by name — requires -parameters compiler flag or @Param. |
| returnObject | The 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
}
}