Spring Boot 8 min read

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.

MR

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.

ApproachProtectsBest For
URL-basedHTTP endpointsAPI routes
Method-levelService methodsBusiness 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

ExpressionDescription
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
permitAllAlways allows
denyAllAlways denies
principalCurrent user principal
authenticationCurrent 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

AnnotationTimingUse Case
@PreAuthorizeBeforeMost common, check before execution
@PostAuthorizeAfterCheck based on return value
@PreFilterBeforeFilter input collections
@PostFilterAfterFilter output collections
@SecuredBeforeSimple role check (no SpEL)
@RolesAllowedBeforeStandard 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

  1. Spring Security Architecture
  2. Database Authentication
  3. OAuth2 & Social Login
  4. Method-Level Security (This Article)
  5. CSRF, CORS & Security Headers
  6. Production Security Best Practices

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

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

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.