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@Configuration@EnableGlobalMethodSecurity
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
}
@EnableMethodSecuritysecuredEnabled = true@Securedjsr250Enabled = true@RolesAllowed@PreAuthorize: Guard Before Execution
@PreAuthorizefalseAccessDeniedException
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
@PostAuthorizereturnObject
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();
}
}
@PostAuthorizeSpEL Reference: Principal, Args & Built-ins
Spring Security exposes a rich set of built-in SpEL functions and objects inside method security expressions:
| Expression | Description |
|---|---|
authentication | The current Authentication |
authentication.name | The authenticated username. |
authentication.principal | The UserDetails |
authentication.principal.id | Any field on your custom principal (e.g., user ID). |
hasRole('ADMIN') | True if the user has ROLE_ADMIN |
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@Param |
returnObject | The method return value (only in @PostAuthorize@PostFilter |
hasPermission(obj, 'read') | Delegates to a custom PermissionEvaluator |
// 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@PostFilterfilterObject
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();
}
}
@PostFilterCustom Permission Evaluator
When simple role/authority checks aren't enough, implement PermissionEvaluatorhasPermission()
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) { ... }
Testing Method Security
Use @WithMockUser@WithMockUser(roles = "ADMIN")ROLE_ADMINspring-security-test
<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
}
}