Spring Security Method-Level Security Annotations

Secure Spring Boot methods with @PreAuthorize, @Secured, @RolesAllowed annotations and SpEL expressions for fine-grained access control.

published: reading time: 24 min read author: GeekWorkBench
Quick Summary

Secure Spring Boot methods with @PreAuthorize, @Secured, @RolesAllowed annotations and SpEL expressions for fine-grained access control. The guide uses practical examples to explain introduction: why method-level security matters, getting started: enabling method security and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Spring Security Method-Level Security Annotations

Method-level security fills the gaps that URL-based security cannot reach. URL security controls which endpoints a user can hit. Method-level annotations decide whether they can actually run a given operation once they are inside. Think building key card versus the specific vault key inside.

We will walk through the four main method-security annotations, when to use each, how SpEL expressions give you fine-grained control, and the failure modes that trip up even experienced developers.


Introduction: Why Method-Level Security Matters

Introduction

Method-level security enforces authorization at the service or operation boundary, where the application knows which resource is being changed and why. This guide compares @PreAuthorize, @PostAuthorize, @Secured, and @RolesAllowed, demonstrates SpEL-based checks, and explains proxy, testing, and failure behavior.

Getting Started: Enabling Method Security

Before using any annotation, you must enable method-level security explicitly in your configuration class.

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    // ...
}

The @EnableMethodSecurity annotation registers the necessary AOP infrastructure. Without it, the annotations have no effect.

For Spring Security 6.x and Spring Boot 3.x, this single annotation replaces the older @EnableGlobalMethodSecurity(prePostEnabled = true) style configuration.


@Secured: Simple Role-Based Checks

The @Secured annotation provides the simplest path into method-level security. It accepts an array of role names, and the method executes only if the authenticated principal possesses at least one of those roles.

@Service
public class UserManagementService {

    @Secured("ROLE_ADMIN")
    public User createUser(User user) {
        // Only principals with ROLE_ADMIN can reach here
        return userRepository.save(user);
    }

    @Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
    public List<User> listUsers() {
        return userRepository.findAll();
    }
}

The ROLE_ Prefix Convention

@Secured expects role names with the ROLE_ prefix already included. When Spring Security checks your authentication, it looks for authorities that exactly match the strings you provide. If your UserDetails returns authorities like ROLE_ADMIN, then @Secured(“ROLE_ADMIN”) works. If it returns just ADMIN, the check fails silently and access is denied.

This trips people up regularly. Check what your UserDetails actually returns.

Limitations of @Secured

@Secured is deliberately minimal. It supports only role checks, no SpEL, no complex expressions, no parameter inspection. When your authorization logic outgrows simple role membership, you will need to reach for @PreAuthorize.


@RolesAllowed: The JSR-250 Standard Equivalent

@RolesAllowed is the Jakarta EE standard annotation for method authorization. Spring Security supports it through the spring-security-config module, and it behaves almost identically to @Secured.

@Service
public class PaymentService {

    @RolesAllowed("ROLE_FINANCE")
    public void processPayment(Payment payment) {
        // Only ROLE_FINANCE principals enter here
    }

    @RolesAllowed({"ROLE_FINANCE", "ROLE_ACCOUNTING"})
    public FinancialReport generateReport() {
        return reportRepository.generate();
    }
}

The @RolesAllowed Rationale

So why does Spring give you both? Standards compliance. @Secured is Spring-specific. @RolesAllowed comes from JSR-250, the Java EE security spec. If you need your code to run in other Jakarta EE containers without changes, @RolesAllowed keeps you aligned with the standard.

Most Spring Boot projects just use @Secured because it is more familiar. The choice between them is mostly stylistic.


@PreAuthorize: Expression-Based Access Control

@PreAuthorize is where things get interesting. Instead of a static list of role names, you write a SpEL expression that Spring evaluates at runtime.

@Service
public class DocumentService {

    @PreAuthorize("hasRole('ADMIN')")
    public Document deleteDocument(Long id) {
        return documentRepository.deleteById(id);
    }

    @PreAuthorize("hasRole('EDITOR') or hasRole('ADMIN')")
    public Document updateDocument(Long id, Document doc) {
        return documentRepository.save(doc);
    }

    @PreAuthorize("hasAuthority('WRITE_DOCUMENTS')")
    public Document createDocument(Document doc) {
        return documentRepository.save(doc);
    }
}

hasRole() vs hasAuthority()

Both check permissions on the current authentication object, but handle prefixes differently:

  • hasRole(‘ADMIN’) prepends ROLE_ internally and checks for ROLE_ADMIN
  • hasAuthority(‘ADMIN’) checks for an exact match on ADMIN

Use hasAuthority() when your UserDetails returns fine-grained permissions like READDOCUMENTS or WRITE_DOCUMENTS. Use hasRole() when you are working with coarse role names that consistently carry the ROLE prefix.

Deny-All and Permit-All

@PreAuthorize("denyAll")
public void unreachableMethod() {
    // No principal can reach here
}

@PreAuthorize("permitAll")
public void publicMethod() {
    // Always accessible, regardless of authentication
}

Handy for marking methods that should never be invoked or that handle auth internally.


SpEL in Security: Beyond Simple Role Checks

SpEL expressions in @PreAuthorize handle far more than role names. You get method parameters, nested property access, custom bean method calls, and boolean logic that can get as complex as you need.

Accessing Method Parameters

Prefix any method parameter with # to reference it in your expression.

@Service
public class AccountService {

    @PreAuthorize("#ownerId == authentication.principal.id or hasRole('ADMIN')")
    public Account getAccount(Long ownerId) {
        return accountRepository.findById(ownerId);
    }

    @PreAuthorize("#amount <= 10000 or hasRole('ADMIN')")
    public TransferResult transfer(Long fromAccount, Long toAccount, BigDecimal amount) {
        // ...
    }
}

The first example checks ownership: a user sees their own account, admins see anything. The second ties a transaction limit to the caller’s role.

Calling Custom Expression Methods

For reusable logic, write a Spring bean method and reference it from your expression.

@Component
public class AuthorizationExpressions {

    public boolean isAccountOwner(Authentication auth, Long ownerId) {
        UserDetails user = (UserDetails) auth.getPrincipal();
        return user.getUsername().equals(ownerRepository.findById(ownerId).getOwnerEmail());
    }

    public boolean canAccessDocument(Authentication auth, Document doc) {
        UserDetails user = (UserDetails) auth.getPrincipal();
        return doc.getVisibility() == Visibility.PUBLIC
            || doc.getOwner().equals(user.getUsername())
            || user.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
}
@Service
public class DocumentService {

    @PreAuthorize("@authzExpressions.isAccountOwner(authentication, #ownerId)")
    public Document getOwnerDocument(Long ownerId) {
        // ...
    }

    @PreAuthorize("@authzExpressions.canAccessDocument(authentication, #doc)")
    public void updateDocument(Document doc) {
        // ...
    }
}

The @ prefix references the bean name, then the method name and parentheses.

Complex Boolean Logic

@PreAuthorize(
    "(hasRole('USER') and #resource.owner == authentication.principal.username) " +
    "or hasAnyRole('ADMIN', 'MODERATOR')"
)
public void modifyResource(Resource resource) {
    // ...
}

Accessing the Authentication Object Directly

Within an expression, authentication binds to the current Authentication object, and principal binds to its principal object (often a UserDetails instance).

@PreAuthorize("authentication.name == 'system'")
public void runSystemTask() {
    // Only the system account can invoke this
}

Mermaid Diagram: The Security Filter Chain and AOP Proxy

The following diagram shows how a secured method call flows through the Spring Security infrastructure:

graph TD
    A[HTTP Request] --> B[Security Filter Chain]
    B --> C{Authentication Filter}
    C -->|Authenticated| D[Controller / Service Bean]
    D --> E["AOP Proxy"]
    E --> F[Authorization Interceptor]
    F --> G["@PreAuthorize / @Secured check"]
    G -->|Access Denied| H[AccessDeniedException → 403]
    G -->|Access Granted| I[Target Method]
    I --> J[Response]
    H --> J

Spring wraps your bean in a proxy at runtime. External calls hit this proxy first, which evaluates the annotation before handing off to the real method.


When to Use Each Annotation

Annotation Best For Limitations
@Secured Simple role checks, legacy compatibility No SpEL, requires ROLE_ prefix
@RolesAllowed JSR-250 compliant code, EE portability Same limitations as @Secured
@PreAuthorize Complex expressions, parameter inspection Slightly more overhead
URL security Protecting routes and endpoints Cannot protect method internals

Use @PreAuthorize when

  • You need to inspect method parameters as part of the authorization decision
  • You want to combine multiple conditions with and, or, not
  • You need custom authorization logic that does not map cleanly to roles
  • You want to call reusable expression methods via @beanName.methodName()

Use @Secured or @RolesAllowed when

  • You only need simple role checks
  • You are working with a codebase that standardizes on one annotation
  • Performance is critical and the minimal overhead of SpEL parsing matters (rare)

Avoid Method Security when

  • Your logic is purely based on URL paths (use SecurityFilterChain instead)
  • The check requires database lookups on every call (consider caching or a different approach)
  • You are securing a read-only operation that can be handled by URL-level authentication

Implementation Snippets

@PreAuthorize with hasRole() and SpEL

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // Additional expression handlers can be registered here
}

@Service
public class OrderService {

    @PreAuthorize("hasRole('CUSTOMER') and #customerId == authentication.principal.id")
    public Order getCustomerOrder(Long customerId, Long orderId) {
        return orderRepository.findById(orderId)
            .filter(o -> o.getCustomerId().equals(customerId))
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }

    @PreAuthorize("hasAnyRole('ADMIN', 'SUPPORT')")
    public Order getAnyOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }
}

Custom @PreAuthorize Expression

@Component("authz")
public class AuthorizationFacade {

    public boolean canManageEmployee(Authentication auth, Long employeeId) {
        UserDetails user = (UserDetails) auth.getPrincipal();
        Employee employee = employeeRepository.findById(employeeId).orElse(null);

        if (employee == null) {
            return false;
        }

        // Users can manage their own profile; admins can manage anyone
        return employee.getEmail().equals(user.getUsername())
            || user.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
}

@Service
public class HrService {

    @PreAuthorize("@authz.canManageEmployee(authentication, #employeeId)")
    public void updateEmployee(Long employeeId, EmployeeUpdate update) {
        // ...
    }
}

Method Security with Method Parameters

@Service
public class FileService {

    @PreAuthorize(
        "#uploaderId == authentication.principal.id " +
        "or hasAnyRole('ADMIN', 'CONTENT_MANAGER')"
    )
    public FileMetadata uploadFile(Long uploaderId, MultipartFile file) {
        // Store file and return metadata
    }

    @PreAuthorize("#ownerId == authentication.principal.id or hasRole('ADMIN')")
    public void deleteFile(Long ownerId, String fileId) {
        fileStorage.delete(fileId);
    }
}

Failure Scenarios

@Async + Method Security: The Security Context Vanishes

When you annotate a method with @Async, Spring executes it in a separate thread pool thread. By default, the SecurityContext from the original request thread does not propagate to the async thread.

@Service
public class NotificationService {

    @Async
    @PreAuthorize("hasRole('ADMIN')")
    public void sendBulkNotification(List<String> userIds, String message) {
        // PROBLEM: authentication is null here in the async thread
        // AccessDeniedException or worse, the @PreAuthorize is never evaluated
    }
}

Fix this by configuring the AsyncTaskExecutor to use a SecurityContext propagator, or by explicitly passing authentication context to the async operation.

@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
        executor.setSecurityContextProvider(new SecurityContextSimpleAsyncTaskExecutor());
        return executor;
    }
}

Self-Invocation: When the Proxy Does Not Intercept

Spring AOP uses proxies. External calls go through the proxy and get intercepted. But when a bean calls its own method directly, it bypasses the proxy entirely.

@Service
public class OrderService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(Long orderId) {
        // This call goes through the proxy
    }

    public void cancelOrder(Long orderId) {
        // This internal call bypasses the proxy
        // @PreAuthorize is NOT evaluated
        deleteOrder(orderId);
    }
}

The cancelOrder method calls deleteOrder directly on this. No proxy intercepts this call, so the @PreAuthorize check never runs.

Solutions include extracting the secured method to a separate bean (so the call crosses bean boundaries and hits the proxy) or using AspectJ weaving instead of Spring AOP proxies.

CSRF with Method Security: Wrong Threat Model

Method security and CSRF protection address different threats. URL security with CSRF tokens prevents cross-site request forgery at the HTTP layer. Method security prevents unauthorized operation execution after authentication.

Do not assume that method-level @PreAuthorize replaces CSRF protection. If your endpoints accept state-changing requests from browsers, you still need CSRF tokens in your HTTP security configuration.

SpEL Injection: When Expressions Come from User Input

Never construct SpEL expressions from untrusted user input.

// DANGEROUS: Never do this
@PreAuthorize("hasRole('" + userProvidedRole + "')")
public void dangerousMethod() {
    // An attacker providing "ADMIN') or hasRole('SUPERUSER" escapes the string
    // and gains elevated access
}

Spring Security’s StringExpressionParser does not guard against injection in the same way parameterized queries protect against SQL injection. Treat any SpEL in annotations as constants, not as anything derived from user input.


Trade-off Table

Feature @Secured @RolesAllowed @PreAuthorize
Simple role check Yes Yes Yes
SpEL expressions No No Yes
Parameter access No No Yes
JSR-250 standard No Yes No
Complex boolean logic No No Yes
Custom bean method calls No No Yes
Method post-processing No No @PostAuthorize
Performance overhead Minimal Minimal Slight (SpEL parsing)
Learning curve Low Low Moderate

Observability Checklist

Securing methods is only half the job. You also need to know when authorization fails and who tried what.

Enable Access Decision Logging

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public DefaultMethodSecurityExpressionHandler expressionHandler() {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(new AuditPermissionEvaluator());
        return handler;
    }
}

Log Access Denied Events

@Component
public class SecurityAuditListener implements ApplicationListener<AuthorizationEvent> {

    private static final Logger log = LoggerFactory.getLogger(SecurityAuditListener.class);

    @Override
    public void onApplicationEvent(AuthorizationEvent event) {
        Authentication auth = event.getAuthentication();
        AuthorizationFailureReason reason = event.getAuthorizationFailureReason();

        log.warn("Access denied for user '{}': {}",
            auth != null ? auth.getName() : "anonymous",
            reason);
    }
}

Structured Audit Trail

For production systems, emit structured audit records whenever a sensitive method is invoked:

@PreAuthorize("hasRole('ADMIN')")
@Audit(event = "ADMIN_ACTION", captureArgs = true)
public void deleteSystemConfiguration(String configKey) {
    // ...
}

Consider using Spring ACD (Annotation-driven Auditing) or a custom aspect to capture who did what and when, without scattering audit logic across every service method.


Security Notes

Method Security vs URL Security

URL security operates at the HTTP layer, before your application code runs. It is fast, declarative, and applies to all requests matching a path pattern. It cannot, however, make decisions based on runtime state like method parameters or database contents.

Method security runs later in the call chain, with full access to the method signature, parameters, and return values. This flexibility comes with more overhead and slightly higher complexity.

Use both. URL security handles the coarse-grained gate. Method security handles the fine-grained checks inside.

Combining with SecurityFilterChain

Method-level annotations work alongside SecurityFilterChain configuration, not in place of it.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults());

    return http.build();
}

Method annotations then layer additional guards on specific operations within those accessible endpoints.

Pre vs Post Authorization

@PreAuthorize evaluates the expression before the method body runs. @PostAuthorize evaluates it after, with access to the return value.

@PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')")
public Document getDocument(Long id) {
    // @PostAuthorize can inspect the returned object
    return documentRepository.findById(id);
}

Default to @PreAuthorize. Reach for @PostAuthorize when the authorization decision actually depends on what the method returns.


Common Pitfalls / Anti-Patterns

The ROLE_ Prefix Inconsistency

Spring Security has a historical quirk where role-checking methods like hasRole() automatically prefix ROLE_, but the annotation strings and UserDetails authorities do not automatically align.

  • @Secured(“ADMIN”) looks for ROLE_ADMIN authority
  • @PreAuthorize(“hasRole(‘ADMIN’)”) looks for ROLE_ADMIN authority
  • @PreAuthorize(“hasAuthority(‘ADMIN’)”) looks for ADMIN authority (no prefix)

Be consistent. If your UserDetails returns ROLE_ADMIN, use hasRole(‘ADMIN’) or @Secured(“ROLE_ADMIN”). If it returns ADMIN, use hasAuthority(‘ADMIN’).

@Async Breaking the Security Context

As covered in the failure scenarios, @Async methods lose the SecurityContext. Configure context propagation explicitly when using @Async with method security.

SpEL Injection Vulnerabilities

Constructing dynamic SpEL from user input creates an injection surface. Keep expressions static. If you need dynamic behavior, call a bean method that encapsulates the logic safely.

Expecting Method Security on Private Methods

Spring AOP proxies can only intercept methods that are externally callable. private methods, constructors, and static methods cannot be secured with these annotations. If you need to secure internal logic, extract it to a separate bean.

Forgetting @EnableMethodSecurity

Without @EnableMethodSecurity, annotations do nothing. This sounds obvious, but it is the most common issue in early setup. If your security annotations seem ignored, double-check that the configuration class has the enabling annotation.


Quick Recap Checklist

  • Enable method security with @EnableMethodSecurity on your configuration class.
  • Use @Secured for simple, static role checks when you do not need SpEL.
  • Use @RolesAllowed for JSR-250 standard compliance in EE environments.
  • Use @PreAuthorize for complex expressions, parameter inspection, and custom logic.
  • Remember that hasRole() adds the ROLE_ prefix; hasAuthority() does not.
  • Access method parameters in expressions using the # prefix.
  • Call custom bean methods from expressions using the @beanName.method() syntax.
  • Watch for self-invocation bypassing the AOP proxy.
  • Configure @Async with SecurityContext propagation if using method security asynchronously.
  • Never construct SpEL expressions from untrusted user input.
  • Use @PostAuthorize when you need to inspect the return value for authorization.
  • Log authorization failures for audit and debugging.
  • Combine URL security and method security for defense in depth.

Interview Questions

1. What is the difference between @PreAuthorize and @Secured in Spring Security?

@Secured supports only simple role-based checks with a fixed list of role names. It does not support SpEL expressions, method parameters, or complex boolean logic.

@PreAuthorize accepts a SpEL expression that Spring evaluates at runtime. This means you can inspect method parameters, call custom bean methods, combine multiple conditions with and/or, and write reusable authorization logic through expression root objects.

In short: @Secured is simpler but limited; @PreAuthorize is more powerful but has a steeper learning curve and slightly more overhead from SpEL parsing.

2. Why does @PreAuthorize("hasRole('ADMIN')") fail even though the user has the ADMIN role?

Spring Security's hasRole() function automatically prefixes its argument with ROLE_ before checking. So hasRole('ADMIN') looks for an authority of ROLE_ADMIN.

If the UserDetails implementation returns authorities as ADMIN (without the prefix) rather than ROLE_ADMIN, the check fails. The fix is either to use hasAuthority('ADMIN') or to ensure the UserDetails returns authorities with the ROLE_ prefix consistently.

3. What is self-invocation in the context of Spring AOP method security?

Spring Security uses AOP proxies to intercept method calls and evaluate security annotations. When a bean calls one of its own annotated methods directly, it bypasses the proxy and the security check never runs.

For example, if OrderService.cancelOrder() internally calls this.deleteOrder(), the @PreAuthorize on deleteOrder is skipped because the call never goes through the proxy.

The solution is to extract the secured method to a separate bean so calls cross the bean boundary and hit the proxy, or switch from Spring AOP to AspectJ weaving which weaves at the bytecode level rather than using proxies.

4. How do you pass method parameters to @PreAuthorize expressions?

Prefix any method parameter with # to make it available in the SpEL expression. For example, @PreAuthorize("#ownerId == authentication.principal.id") accesses the ownerId parameter directly.

Multiple parameters are supported: @PreAuthorize("#customerId == authentication.principal.id and #amount <= 1000").

You can also pass the entire parameter object: @PreAuthorize("@authz.canAccessDocument(authentication, #doc)") where #doc is the parameter and @authz references a Spring bean with the canAccessDocument method.

5. How does @Async break method-level security and how do you fix it?

When a method annotated with @Async runs, Spring executes it in a separate thread from a thread pool. The SecurityContext is stored in a ThreadLocal, so it does not automatically propagate to the new thread. This means authentication is null inside the async method, and any @PreAuthorize check fails.

To fix this, configure your AsyncTaskExecutor to propagate the security context:

  • Implement AsyncConfigurer and override getAsyncExecutor()
  • Wrap the executor with a SecurityContextAsyncTaskExecutor
  • Or use setSecurityContextProvider() on a SimpleAsyncTaskExecutor

Alternatively, pass necessary authentication data as method parameters and perform the authorization check in the calling thread before launching the async operation.

6. What is the difference between @PreAuthorize and @PostAuthorize? When would you use each?

@PreAuthorize evaluates authorization before the method body executes. Use it when the decision can be made based on the request context, method parameters, or principal properties alone.

@PostAuthorize evaluates authorization after the method runs, with access to the return value. Use it when the authorization decision actually depends on what the method returns, such as filtering a result set based on the caller's ownership of the returned object.

Example: @PostAuthorize("returnObject.owner == authentication.principal.username") inspects the returned entity to verify the caller owns it. This cannot be done with @PreAuthorize because the object does not exist until the method executes.

Default to @PreAuthorize. Only reach for @PostAuthorize when the return value is essential to the authorization decision.

7. What is the ROLE_ prefix convention in Spring Security and why does it exist?

Spring Security distinguishes between roles and authorities as separate concepts. Roles represent coarse-grained groupings like ADMIN or USER, while authorities represent fine-grained permissions like READ_DOCUMENTS or WRITE_CONFIG.

By convention, roles carry the ROLE_ prefix. This allows Spring Security to apply role-specific logic like the hasRole() method, which automatically prepends ROLE_ before performing the lookup. This prevents accidental overlap between roles and permissions in expression-based security.

Key rules: hasRole('ADMIN') looks for ROLE_ADMIN; hasAuthority('ADMIN') looks for an exact ADMIN match. Mixing these up is the most common source of authorization failures in Spring Security method security.

8. How does Spring Security evaluate @PreAuthorize expressions internally?

When @EnableMethodSecurity is present, Spring registers an AOP Alliance method interceptor around every bean method. Before the method executes, the interceptor:

  • Extracts the SpEL expression from the annotation
  • Creates a MethodSecurityExpressionHandler with the current Authentication object as the expression root
  • Sets up the expression context with references to authentication, principal, method, and any #parameter names from the method signature
  • Evaluates the expression to a boolean
  • Throws AccessDeniedException if false, or proceeds to the target method if true

The SpEL expression is parsed and compiled on first use, then cached for subsequent calls to minimize overhead.

9. What is the SecurityContext and why is it critical for method-level security?

The SecurityContext is a ThreadLocal holder for the current Authentication object. It is populated by the SecurityFilterChain during request authentication and is what method security annotations inspect when evaluating authorization rules.

It is critical because method security interceptors read from SecurityContextHolder.getContext().getAuthentication() to obtain the current principal and their granted authorities. If the SecurityContext is null or contains an unauthenticated principal, authorization checks fail or deny access unexpectedly.

Common pitfalls: the SecurityContext does not propagate to child threads (async), it does not propagate across thread boundaries in @Scheduled tasks unless configured, and it is lost on the principal transition between synchronous and reactive stacks.

10. How do you debug a method security annotation that appears to be ignored?

Start with this checklist in order:

  • Is @EnableMethodSecurity present? Without it, annotations have no effect. This is the most common cause of ignored annotations.
  • Is the call going through the proxy? Self-invocation (calling within the same bean) bypasses the proxy entirely. Verify the call comes from an external client or another bean.
  • Is the principal authenticated? Anonymous users have no roles. Even a fully authorized principal will fail if the authentication object is null.
  • Is the ROLE_ prefix correct? If UserDetails returns ADMIN but hasRole('ADMIN') looks for ROLE_ADMIN, the check fails silently.
  • Is @Async involved? The security context does not propagate into async threads by default, so method security runs with a null authentication.
  • Enable DEBUG logging on org.springframework.security to see the actual authorization decisions being made.
11. Can you secure private methods with @PreAuthorize? Why or why not?

No. Spring Security method security relies on Spring AOP, which works through JDK dynamic proxies or CGLIB proxies. Both proxy mechanisms can only intercept calls that go through the proxy — meaning the method must be externally callable on the bean.

Private methods are called directly on the target object within the bean, never crossing the proxy boundary. Similarly, static methods and final methods on classes cannot be intercepted.

If you need to secure internal logic, extract it to a separate bean so that calls to the secured method cross the bean boundary and are properly intercepted by the proxy.

12. How does method security interact with Spring's @Transactional annotation?

Both annotations use AOP, and their order matters. Spring resolves this through a predefined advisor ordering. Transactional advice typically runs before security advice because it needs a clean connection to work with. This means:

  • If a method is denied access by @PreAuthorize, the transaction never starts — which is correct behavior.
  • If the method passes security but throws an exception, the transaction is still rolled back as expected.

However, if you use @PostAuthorize, the transaction has already committed by the time the post-authorization check runs. If @PostAuthorize throws AccessDeniedException after a transactional method succeeds, the data has already been persisted and cannot be rolled back by Spring's transaction infrastructure.

Avoid combining @Transactional with @PostAuthorize on methods that modify data. Use @PreAuthorize instead for authorization that must gate data changes.

13. What is the performance impact of @PreAuthorize compared to @Secured?

@Secured performs a simple string comparison against the principal's granted authorities. This is O(n) where n is the number of authorities, but since authority lists are typically small (a handful of roles), the impact is negligible.

@PreAuthorize with SpEL introduces two overhead sources:

  • Expression parsing: The SpEL expression string is parsed into an AST on first evaluation. Spring caches the compiled expression, so this cost is amortized across calls.
  • Expression evaluation: Each invocation evaluates the expression against the current security context. Simple expressions like hasRole('ADMIN') are fast. Complex expressions with method calls or nested property access add more overhead.

For most applications, the overhead is unmeasurable compared to the database and network latency in a typical request. Only in high-throughput, latency-sensitive code paths should this be a concern, and in those cases, caching the authorization result for the duration of the request is a common mitigation.

14. What is the difference between hasRole(), hasAuthority(), and hasAnyRole() in SpEL expressions?

hasRole(String role) — Checks if the current principal has a role with the given name. It automatically prefixes the argument with ROLE_, so hasRole('ADMIN') checks for ROLE_ADMIN.

hasAuthority(String authority) — Checks for an exact match on the authority name without any prefix transformation. Use this when your UserDetails returns fine-grained permissions that do not follow the role naming convention.

hasAnyRole(String... roles) — Short-circuit OR across multiple roles. Like hasRole, each argument is prefixed with ROLE_. Equivalent to writing hasRole('A') or hasRole('B') but more concise.

There is no built-in hasAnyAuthority() function — you would need to chain multiple hasAuthority() calls with or.

15. How does method-level security work in a Spring Reactive (WebFlux) application?

Spring Security 5.2+ introduced reactive support for method security through @EnableReactiveMethodSecurity. Instead of ThreadLocal-based SecurityContext, reactive applications use ReactiveSecurityContextHolder which propagates context through the reactive pipeline via SubscriberContext.

The same annotations (@PreAuthorize, @Secured, etc.) work, but the authorization check returns a Mono<Boolean> that is resolved as part of the reactive pipeline. The method itself should return a reactive type (Mono or Flux), and the security check is applied per-item for Flux.

Key difference: the security context is not evaluated eagerly on a different thread. It is resolved from the reactive context when the pipeline executes, so there is no @Async problem in reactive applications — but context propagation still depends on the reactive context being properly set up by the security filter chain.

Further Reading


Conclusion

Method-level security fills the gaps that URL-based security leaves open. URL rules gate which endpoints a user can reach. Annotations on individual methods decide what they can actually do once inside.

@Secured and @RolesAllowed handle simple role checks with minimal overhead. @PreAuthorize unlocks full SpEL expressiveness, letting you inspect parameters, call custom logic, and compose complex conditions. The tradeoff is a small amount of SpEL parsing overhead and a steeper learning curve.

The most common mistakes are the ROLE_ prefix mismatch, self-invocation bypassing the proxy, and @Async losing the security context. None of these are obvious until they bite you in production. Now you know where to look.

Layer method security on top of your SecurityFilterChain configuration, not instead of it. They reinforce each other.


Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path