Method Security: @PreAuthorize, @PostAuthorize, @Secured
Why Method Security Exists
URL-based authorization in authorizeHttpRequests() protects HTTP endpoints, but it has blind spots:
- The same service method may be called from multiple controllers, scheduled tasks, or message listeners — none of which pass through the HTTP filter chain
- Fine-grained rules based on method arguments or return values cannot be expressed as URL patterns
- Authorization logic spread across a large security config is hard to read alongside the code it protects
Method security moves the authorization decision to the method itself. A @PreAuthorize annotation on a service method is enforced regardless of how that method is called.
Enabling Method Security
Add @EnableMethodSecurity to any @Configuration class:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter
public class SecurityConfig {
// ...
}
@EnableMethodSecurity replaces the older @EnableGlobalMethodSecurity (deprecated in Spring Security 6). By default it enables pre/post annotations. To also enable @Secured and JSR-250 annotations (@RolesAllowed):
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
@PreAuthorize
@PreAuthorize evaluates a SpEL expression before the method executes. If the expression is false, a AccessDeniedException is thrown and the method body never runs.
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@PreAuthorize("hasAuthority('user:read')")
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
}
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
}
}
The third example shows the key power: #username binds to the method parameter username, and authentication.name is the currently logged-in user’s name. Users can read their own profile; admins can read anyone’s.
Accessing Method Parameters in SpEL
Parameter names are available via #paramName syntax. Spring resolves them using reflection with -parameters compiler flag or @P annotation as a fallback:
// Works when compiled with -parameters (Spring Boot default)
@PreAuthorize("hasRole('MANAGER') or #userId == authentication.principal.id")
public void updateUser(Long userId, UserUpdateRequest request) { ... }
// Explicit parameter name binding with @P
@PreAuthorize("hasRole('MANAGER') or #id == authentication.principal.id")
public void updateUser(@P("id") Long userId, UserUpdateRequest request) { ... }
Checking Object Properties
If the parameter is an object, you can navigate its properties:
@PreAuthorize("hasRole('ADMIN') or #order.customerId == authentication.principal.id")
public void cancelOrder(Order order) { ... }
@PostAuthorize
@PostAuthorize evaluates after the method returns, with access to the return value via returnObject. Use it when the access decision depends on what the method returned.
@Service
public class DocumentService {
@PostAuthorize("hasRole('ADMIN') or returnObject.ownerId == authentication.principal.id")
public Document getDocument(Long id) {
return documentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Document not found"));
}
}
The method executes and loads the document from the database. The authorization check then verifies whether the caller is the document owner. If not, AccessDeniedException is thrown — but note that the method did execute (and any side effects occurred). Use @PostAuthorize only for read operations or when pre-execution side effects are acceptable.
@PreFilter and @PostFilter
These filter collection arguments or return values rather than blocking the entire call.
@PostFilter
Filters the returned collection, keeping only elements for which the expression is true:
@PostFilter("hasRole('ADMIN') or filterObject.ownerId == authentication.principal.id")
public List<Document> getAllDocuments() {
return documentRepository.findAll(); // loads all, then filters in memory
}
filterObject refers to each element in the collection. After the method returns, Spring removes any element for which the expression evaluates to false.
Warning: @PostFilter loads the full dataset and filters in memory. For large collections, filter in the query instead:
// Prefer this — filter in the database
@PreAuthorize("isAuthenticated()")
public List<Document> getDocuments(Authentication auth) {
if (hasRole(auth, "ADMIN")) return documentRepository.findAll();
return documentRepository.findByOwnerId(auth.getName());
}
@PreFilter
Filters a collection argument before the method executes:
@PreFilter("filterObject.ownerId == authentication.principal.id")
public void deleteDocuments(List<Document> documents) {
documentRepository.deleteAll(documents); // only receives documents the caller owns
}
When a method has multiple collection parameters, specify which one to filter:
@PreFilter(filterTarget = "documents", value = "filterObject.status == 'DRAFT'")
public void publishDocuments(List<Document> documents, PublishConfig config) { ... }
@Secured
@Secured is simpler than @PreAuthorize — it takes a list of role strings and requires the user to have at least one of them. No SpEL support.
@Secured("ROLE_ADMIN")
public void deleteUser(Long id) { ... }
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public List<Report> getReports() { ... }
Enable with @EnableMethodSecurity(securedEnabled = true). Prefer @PreAuthorize for new code — @Secured exists mainly for compatibility.
@RolesAllowed (JSR-250)
The standard Java EE / Jakarta EE annotation. Functionally equivalent to @Secured:
@RolesAllowed("ROLE_ADMIN")
public void deleteUser(Long id) { ... }
@RolesAllowed({"ROLE_ADMIN", "ROLE_MANAGER"})
public List<Report> getReports() { ... }
Enable with @EnableMethodSecurity(jsr250Enabled = true). Useful if you want to keep your service layer free of Spring-specific annotations.
Built-in SpEL Expressions
Spring Security provides these built-in expressions for use in @PreAuthorize and @PostAuthorize:
| Expression | Description |
|---|---|
hasRole('X') | User has ROLE_X |
hasAnyRole('X', 'Y') | User has ROLE_X or ROLE_Y |
hasAuthority('X') | User has authority X (exact match) |
hasAnyAuthority('X', 'Y') | User has X or Y |
isAuthenticated() | User is logged in (not anonymous) |
isAnonymous() | User is anonymous |
isFullyAuthenticated() | Authenticated and not via remember-me |
isRememberMe() | Authenticated via remember-me token |
permitAll | Always true |
denyAll | Always false |
authentication | The current Authentication object |
principal | authentication.getPrincipal() |
Custom SpEL Beans
For complex authorization logic, delegate to a Spring bean from SpEL using the @beanName.method() syntax:
@Component("authz")
public class AuthorizationHelper {
private final ProjectRepository projectRepository;
public AuthorizationHelper(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
public boolean isProjectMember(Authentication auth, Long projectId) {
return projectRepository.isMember(auth.getName(), projectId);
}
public boolean isProjectOwner(Authentication auth, Long projectId) {
return projectRepository.isOwner(auth.getName(), projectId);
}
}
@Service
public class ProjectService {
@PreAuthorize("hasRole('ADMIN') or @authz.isProjectMember(authentication, #projectId)")
public Project getProject(Long projectId) { ... }
@PreAuthorize("hasRole('ADMIN') or @authz.isProjectOwner(authentication, #projectId)")
public void deleteProject(Long projectId) { ... }
}
This keeps complex database lookups out of annotation strings and makes authorization logic unit-testable.
Custom Meta-Annotations
Repeating @PreAuthorize("hasRole('ADMIN')") across dozens of methods is noisy. Create custom meta-annotations:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public @interface IsSelfOrAdmin {
// Can't parameterize SpEL from meta-annotations — use fixed param name convention
}
Usage:
@IsAdmin
public void deleteUser(Long id) { ... }
@IsAdmin
public List<User> getAllUsers() { ... }
You can also annotate at the class level — every method in the class inherits the constraint:
@RestController
@PreAuthorize("hasRole('ADMIN')") // all methods require ADMIN
@RequestMapping("/admin")
public class AdminController {
public List<User> listUsers() { ... } // inherits ADMIN requirement
@PreAuthorize("hasRole('SUPER_ADMIN')") // method annotation overrides class annotation
public void nukeDatabase() { ... }
}
Handling AccessDeniedException
When @PreAuthorize denies access, Spring throws AccessDeniedException. For REST APIs, handle it globally:
@RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("Access denied", ex.getMessage()));
}
}
Spring Security’s ExceptionTranslationFilter also handles AccessDeniedException for requests that pass through the filter chain — for non-HTTP invocations (scheduled jobs, message listeners), the exception propagates up to your calling code.
Testing Method Security
Use @WithMockUser to simulate a logged-in user in tests:
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanGetAllUsers() {
assertDoesNotThrow(() -> userService.getAllUsers());
}
@Test
@WithMockUser(roles = "USER")
void regularUserCannotGetAllUsers() {
assertThrows(AccessDeniedException.class, () -> userService.getAllUsers());
}
@Test
@WithMockUser(username = "alice", roles = "USER")
void userCanReadOwnProfile() {
assertDoesNotThrow(() -> userService.getUserByUsername("alice"));
}
@Test
@WithMockUser(username = "alice", roles = "USER")
void userCannotReadOtherProfile() {
assertThrows(AccessDeniedException.class,
() -> userService.getUserByUsername("bob"));
}
}
Method Security Internals
Method security is implemented via Spring AOP. When @EnableMethodSecurity is active, Spring wraps annotated beans in a proxy. Every call to an annotated method goes through AuthorizationManagerBeforeMethodInterceptor (for @PreAuthorize) and AuthorizationManagerAfterMethodInterceptor (for @PostAuthorize).
flowchart LR
Caller --> Proxy[Spring AOP Proxy]
Proxy --> Pre[PreAuthorize\nInterceptor]
Pre -->|Denied| Ex[AccessDeniedException]
Pre -->|Allowed| Method[Actual Method]
Method --> Post[PostAuthorize\nInterceptor]
Post -->|Denied| Ex
Post -->|Allowed| Caller
Because this is proxy-based, self-invocation does not trigger security checks:
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }
public void processOrders() {
deleteOrder(1L); // BYPASSES @PreAuthorize — same class, no proxy involved
}
}
Fix by injecting the bean into itself (or extracting to a separate class):
@Service
public class OrderService {
@Autowired
private OrderService self; // inject own proxy
public void processOrders() {
self.deleteOrder(1L); // goes through proxy — @PreAuthorize is enforced
}
}
Key Takeaways
@EnableMethodSecurityactivates method-level authorization across the entire application@PreAuthorizeis the primary annotation — it runs before the method and supports full SpEL@PostAuthorizeruns after the method and can inspect the return value — use only for reads@PreFilter/@PostFilterfilter collections rather than blocking the call entirely- Delegate to
@Componentbeans from SpEL for complex, testable authorization logic - Method security uses AOP proxies — self-invocations within the same bean bypass all annotations
- Class-level annotations apply to all methods; method annotations override class annotations
Next: Domain Object Security: Access Control Lists (ACLs) — per-object permissions for fine-grained data-level access control.