Spring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Moshiour Rahman
Advertisement
Why Method-Level Security?
URL-based security (requestMatchers()) protects endpoints, but method-level security protects business logic regardless of how it’s invoked.
| Approach | Protects | Best For |
|---|---|---|
| URL-based | HTTP endpoints | API routes |
| Method-level | Service methods | Business logic |
Method-level security ensures authorization even when methods are called internally, from schedulers, or message handlers.
Enabling Method Security
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // @PreAuthorize, @PostAuthorize
securedEnabled = true, // @Secured
jsr250Enabled = true // @RolesAllowed
)
public class MethodSecurityConfig {
}
Basic Annotations
@PreAuthorize
Evaluated before method execution. Most commonly used.
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@PreAuthorize("hasAuthority('DELETE_USER')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
@PostAuthorize
Evaluated after method execution. Access to return value.
@Service
public class DocumentService {
@PostAuthorize("returnObject.owner.username == authentication.name")
public Document getDocument(Long id) {
return documentRepository.findById(id)
.orElseThrow(() -> new DocumentNotFoundException(id));
}
@PostAuthorize("returnObject.status != 'CONFIDENTIAL' or hasRole('ADMIN')")
public Report getReport(Long id) {
return reportRepository.findById(id)
.orElseThrow(() -> new ReportNotFoundException(id));
}
}
@Secured
Simple role-based. No SpEL support.
@Service
public class LegacyService {
@Secured("ROLE_ADMIN")
public void adminOnlyMethod() {
// Admin only
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManagerMethod() {
// Admin or Manager
}
}
@RolesAllowed (JSR-250)
Standard Java annotation. Same as @Secured.
@Service
public class StandardService {
@RolesAllowed("ADMIN")
public void adminMethod() {
// Admin only
}
@RolesAllowed({"ADMIN", "MANAGER"})
public void multiRoleMethod() {
// Multiple roles
}
}
SpEL Expressions Deep Dive
Spring Expression Language (SpEL) provides powerful authorization expressions.
Built-in Expressions
| Expression | Description |
|---|---|
hasRole('ROLE') | Has specific role (auto-prefixes ROLE_) |
hasAnyRole('R1', 'R2') | Has any of the roles |
hasAuthority('AUTH') | Has specific authority (exact match) |
hasAnyAuthority('A1', 'A2') | Has any of the authorities |
isAuthenticated() | User is authenticated |
isAnonymous() | User is anonymous |
isRememberMe() | Logged in via remember-me |
isFullyAuthenticated() | Not anonymous, not remember-me |
permitAll | Always allows |
denyAll | Always denies |
principal | Current user principal |
authentication | Current Authentication object |
Accessing Method Parameters
Use #paramName to access method parameters:
@Service
public class ProjectService {
@PreAuthorize("#username == authentication.name")
public UserProfile getProfile(String username) {
return profileRepository.findByUsername(username);
}
@PreAuthorize("#project.owner.username == authentication.name or hasRole('ADMIN')")
public void updateProject(Project project) {
projectRepository.save(project);
}
@PreAuthorize("@projectSecurityService.canAccess(#projectId, authentication)")
public Project getProject(Long projectId) {
return projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectNotFoundException(projectId));
}
}
Complex Expressions
@Service
public class OrderService {
// Multiple conditions
@PreAuthorize("""
hasRole('ADMIN') or
(hasRole('MANAGER') and #order.department == authentication.principal.department)
""")
public void approveOrder(Order order) {
order.setStatus(OrderStatus.APPROVED);
orderRepository.save(order);
}
// Null-safe checks
@PreAuthorize("#order?.customerId == authentication.principal.customerId or hasRole('ADMIN')")
public void cancelOrder(Order order) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
// Collection checks
@PreAuthorize("authentication.principal.managedDepartments.contains(#departmentId)")
public List<Order> getDepartmentOrders(String departmentId) {
return orderRepository.findByDepartmentId(departmentId);
}
}
Custom Security Expressions
Permission Evaluator
For complex authorization logic, create a custom PermissionEvaluator:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private ProjectRepository projectRepository;
@Autowired
private DocumentRepository documentRepository;
@Override
public boolean hasPermission(
Authentication authentication,
Object targetDomainObject,
Object permission) {
if (authentication == null || targetDomainObject == null) {
return false;
}
String permissionString = permission.toString().toUpperCase();
if (targetDomainObject instanceof Project) {
return hasProjectPermission(authentication, (Project) targetDomainObject, permissionString);
}
if (targetDomainObject instanceof Document) {
return hasDocumentPermission(authentication, (Document) targetDomainObject, permissionString);
}
return false;
}
@Override
public boolean hasPermission(
Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
if (authentication == null || targetId == null || targetType == null) {
return false;
}
String permissionString = permission.toString().toUpperCase();
if ("PROJECT".equalsIgnoreCase(targetType)) {
Project project = projectRepository.findById((Long) targetId).orElse(null);
return project != null && hasProjectPermission(authentication, project, permissionString);
}
if ("DOCUMENT".equalsIgnoreCase(targetType)) {
Document document = documentRepository.findById((Long) targetId).orElse(null);
return document != null && hasDocumentPermission(authentication, document, permissionString);
}
return false;
}
private boolean hasProjectPermission(
Authentication authentication,
Project project,
String permission) {
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
return switch (permission) {
case "READ" -> project.isPublic() ||
project.getMembers().contains(user.getId()) ||
isAdmin(authentication);
case "WRITE" -> project.getOwner().getId().equals(user.getId()) ||
project.getEditors().contains(user.getId()) ||
isAdmin(authentication);
case "DELETE" -> project.getOwner().getId().equals(user.getId()) ||
isAdmin(authentication);
case "ADMIN" -> project.getOwner().getId().equals(user.getId());
default -> false;
};
}
private boolean hasDocumentPermission(
Authentication authentication,
Document document,
String permission) {
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
return switch (permission) {
case "READ" -> document.getVisibility() == Visibility.PUBLIC ||
document.getOwner().getId().equals(user.getId()) ||
document.getSharedWith().contains(user.getId());
case "WRITE", "DELETE" ->
document.getOwner().getId().equals(user.getId()) ||
isAdmin(authentication);
default -> false;
};
}
private boolean isAdmin(Authentication authentication) {
return authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
}
Register Permission Evaluator
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
CustomPermissionEvaluator permissionEvaluator) {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
Using hasPermission
@Service
public class ProjectService {
@PreAuthorize("hasPermission(#project, 'WRITE')")
public void updateProject(Project project) {
projectRepository.save(project);
}
@PreAuthorize("hasPermission(#projectId, 'PROJECT', 'READ')")
public Project getProject(Long projectId) {
return projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectNotFoundException(projectId));
}
@PreAuthorize("hasPermission(#projectId, 'PROJECT', 'DELETE')")
public void deleteProject(Long projectId) {
projectRepository.deleteById(projectId);
}
}
Custom Security Beans
Reference Spring beans in SpEL with @beanName:
@Component("projectSecurity")
public class ProjectSecurityService {
private final ProjectRepository projectRepository;
private final ProjectMemberRepository memberRepository;
public ProjectSecurityService(
ProjectRepository projectRepository,
ProjectMemberRepository memberRepository) {
this.projectRepository = projectRepository;
this.memberRepository = memberRepository;
}
public boolean isOwner(Long projectId, Authentication authentication) {
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
return projectRepository.findById(projectId)
.map(project -> project.getOwner().getId().equals(user.getId()))
.orElse(false);
}
public boolean isMember(Long projectId, Authentication authentication) {
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
return memberRepository.existsByProjectIdAndUserId(projectId, user.getId());
}
public boolean canEdit(Long projectId, Authentication authentication) {
return isOwner(projectId, authentication) ||
memberRepository.isEditor(projectId,
((UserPrincipal) authentication.getPrincipal()).getId());
}
public boolean canAccessSensitiveData(Project project, Authentication auth) {
if (project.getSecurityLevel() == SecurityLevel.PUBLIC) {
return true;
}
if (project.getSecurityLevel() == SecurityLevel.INTERNAL) {
return auth.isAuthenticated();
}
// CONFIDENTIAL - only admins and owners
return isOwner(project.getId(), auth) ||
auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
}
Using Custom Bean
@Service
public class ProjectService {
@PreAuthorize("@projectSecurity.isOwner(#projectId, authentication)")
public void deleteProject(Long projectId) {
projectRepository.deleteById(projectId);
}
@PreAuthorize("@projectSecurity.isMember(#projectId, authentication)")
public Project getProject(Long projectId) {
return projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectNotFoundException(projectId));
}
@PreAuthorize("@projectSecurity.canEdit(#projectId, authentication)")
public void updateProject(Long projectId, ProjectUpdateRequest request) {
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectNotFoundException(projectId));
project.setName(request.getName());
project.setDescription(request.getDescription());
projectRepository.save(project);
}
}
@PreFilter and @PostFilter
Filter collections based on security rules.
@PreFilter
Filter input collections before method execution:
@Service
public class BatchService {
@PreFilter("filterObject.owner.username == authentication.name or hasRole('ADMIN')")
public void deleteDocuments(List<Document> documents) {
documentRepository.deleteAll(documents);
}
@PreFilter(
filterTarget = "documentIds",
value = "@documentSecurity.canDelete(filterObject, authentication)"
)
public void deleteByIds(List<Long> documentIds, String reason) {
documentRepository.deleteAllById(documentIds);
auditService.logBatchDelete(documentIds, reason);
}
}
@PostFilter
Filter return collections after method execution:
@Service
public class DocumentService {
@PostFilter("filterObject.owner.username == authentication.name or filterObject.isPublic")
public List<Document> getAllDocuments() {
return documentRepository.findAll();
}
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Project> getUserProjects() {
return projectRepository.findAll();
}
// Combine with @PreAuthorize
@PreAuthorize("isAuthenticated()")
@PostFilter("filterObject.visibility == 'PUBLIC' or filterObject.department == authentication.principal.department")
public List<Report> getReports() {
return reportRepository.findAll();
}
}
Controller-Level Security
Apply security directly on controllers:
@RestController
@RequestMapping("/api/projects")
@PreAuthorize("isAuthenticated()") // Applies to all methods
public class ProjectController {
private final ProjectService projectService;
@GetMapping
@PostFilter("filterObject.isPublic or filterObject.members.contains(authentication.principal.id)")
public List<ProjectDTO> getAllProjects() {
return projectService.findAll();
}
@GetMapping("/{id}")
@PreAuthorize("@projectSecurity.canAccess(#id, authentication)")
public ProjectDTO getProject(@PathVariable Long id) {
return projectService.findById(id);
}
@PostMapping
@PreAuthorize("hasAnyRole('ADMIN', 'PROJECT_MANAGER')")
public ProjectDTO createProject(@Valid @RequestBody CreateProjectRequest request) {
return projectService.create(request);
}
@PutMapping("/{id}")
@PreAuthorize("@projectSecurity.canEdit(#id, authentication)")
public ProjectDTO updateProject(
@PathVariable Long id,
@Valid @RequestBody UpdateProjectRequest request) {
return projectService.update(id, request);
}
@DeleteMapping("/{id}")
@PreAuthorize("@projectSecurity.isOwner(#id, authentication) or hasRole('ADMIN')")
public void deleteProject(@PathVariable Long id) {
projectService.delete(id);
}
}
Meta-Annotations
Create reusable security 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 hasRole('MANAGER')")
public @interface IsAdminOrManager {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@projectSecurity.isOwner(#projectId, authentication)")
public @interface IsProjectOwner {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasPermission(#id, 'PROJECT', 'READ')")
public @interface CanReadProject {
}
Using Meta-Annotations
@Service
public class AdminService {
@IsAdmin
public void performAdminAction() {
// Only admins
}
@IsAdminOrManager
public void performManagementAction() {
// Admins or managers
}
}
@Service
public class ProjectService {
@IsProjectOwner
public void deleteProject(Long projectId) {
projectRepository.deleteById(projectId);
}
@CanReadProject
public Project getProject(Long id) {
return projectRepository.findById(id).orElseThrow();
}
}
Testing Method Security
@SpringBootTest
@AutoConfigureMockMvc
class ProjectSecurityTest {
@Autowired
private ProjectService projectService;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanDeleteAnyProject() {
assertDoesNotThrow(() -> projectService.deleteProject(1L));
}
@Test
@WithMockUser(username = "user", roles = "USER")
void userCannotDeleteOthersProject() {
assertThrows(AccessDeniedException.class,
() -> projectService.deleteProject(1L));
}
@Test
@WithMockUser(username = "owner@example.com")
void ownerCanDeleteOwnProject() {
// Assuming project 1 is owned by owner@example.com
assertDoesNotThrow(() -> projectService.deleteProject(1L));
}
@Test
void anonymousUserCannotAccessProtectedMethod() {
assertThrows(AuthenticationException.class,
() -> projectService.getProject(1L));
}
}
// Custom annotation for specific user
@WithSecurityContext(factory = WithProjectOwnerSecurityContextFactory.class)
@interface WithProjectOwner {
long projectId();
}
class WithProjectOwnerSecurityContextFactory
implements WithSecurityContextFactory<WithProjectOwner> {
@Override
public SecurityContext createSecurityContext(WithProjectOwner annotation) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
// Create authentication for project owner
UserPrincipal principal = createProjectOwner(annotation.projectId());
Authentication auth = new UsernamePasswordAuthenticationToken(
principal, null, principal.getAuthorities());
context.setAuthentication(auth);
return context;
}
}
Summary
| Annotation | Timing | Use Case |
|---|---|---|
@PreAuthorize | Before | Most common, check before execution |
@PostAuthorize | After | Check based on return value |
@PreFilter | Before | Filter input collections |
@PostFilter | After | Filter output collections |
@Secured | Before | Simple role check (no SpEL) |
@RolesAllowed | Before | Standard Java (JSR-250) |
Method-level security provides fine-grained access control for your business logic. In the next article, we’ll cover CSRF, CORS, and security headers.
Series Navigation
- Spring Security Architecture
- Database Authentication
- OAuth2 & Social Login
- Method-Level Security (This Article)
- CSRF, CORS & Security Headers
- Production Security Best Practices
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Spring Security Architecture: Complete Guide to How Security Works
Master Spring Security internals. Learn FilterChain, SecurityContext, Authentication flow, and how Spring Security protects your application.
Spring BootSpring Security CSRF, CORS & Headers: Complete Protection Guide
Master CSRF protection, CORS configuration, and security headers in Spring Boot. Learn when to disable CSRF, configure CORS properly, and add security headers.
Spring BootSpring Security OAuth2: Complete Social Login Guide (Google, GitHub)
Implement OAuth2 social login in Spring Boot. Learn Google, GitHub authentication, custom OAuth2 providers, and combining with JWT.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.