skip to content

A @PreAuthorize annotation on a service method is being ignored at runtime. What are the likely causes given how @EnableMethodSecurity works?

level: middleimportance: should knowfreq 50%

answer

  1. Proxy boundary — call must cross it
  2. Self-invocation this.x() bypasses (like @Transactional)
  3. Not a bean / private / final = no advice
  4. Missing flag → annotation silently ignored
  5. Fix: separate bean, self-inject, or AopContext.currentProxy()

basics

~20 s

Method security uses AOP proxies. The check only runs when the call comes through the proxy. Common misses: calling the method from within the same bean (self-invocation), the class isn't a Spring bean, the method is private/final, or you forgot @EnableMethodSecurity / the right flag.

solid answer

~50 s

@EnableMethodSecurity enforces checks with Spring AOP proxies, so a @PreAuthorize only fires when the invocation goes through the proxy. The classic cause of a silently ignored annotation is self-invocation: one method calling another method of the same bean via this.method() bypasses the proxy entirely, so no interceptor runs. Other causes: the annotated class isn't a Spring-managed bean (you new-ed it); the method is private, final, or package-private so the proxy can't advise it; @EnableMethodSecurity is missing or the relevant flag is off (e.g. using @Secured without securedEnabled=true — unrecognized annotations are silently ignored, no error); or the object is a proxy but the call originates internally. Fixes: move the secured method to a separate bean and inject it, self-inject the bean, or use AopContext.currentProxy() with exposeProxy. Verify the bean is proxied and the annotation family is enabled.

code

java · 15 lines
java
@Service
class OrderService {
    private final AuditService audit;   // extract secured method to its own bean
    OrderService(AuditService audit) { this.audit = audit; }

    public void process() {
        audit.record();   // cross-bean call goes THROUGH AuditService's proxy → check runs
    }
}

@Service
class AuditService {
    @PreAuthorize("hasRole('ADMIN')")
    public void record() { /* ... */ }
}

go deeper

for a junior

Know the check only runs when the method is called from outside the bean.

for a middle

List the proxy-related causes (self-invocation, non-bean, private/final, missing flag) and give fixes.

for a senior

Connect it to the shared proxy limitation with @Transactional and choose the cleanest structural fix.

for a principal

Decide policy: prefer bean extraction over AopContext hacks; weigh AspectJ mode's cost/benefit for removing the limitation.

## Why annotations get silently ignored Because `@EnableMethodSecurity` works via **Spring AOP proxies**, the security advice only executes when the call **crosses the proxy boundary**. Any call path that doesn't go through the proxy skips the check — and crucially, this fails **silently** (the method just runs unguarded; no exception, no warning). ## The usual suspects 1. **Self-invocation (most common).** Within a bean, `this.secured()` (or an unqualified `secured()`) calls the *raw target instance*, not the proxy, so the interceptor never runs. ```java @Service class OrderService { public void process() { audit(); } // internal call → no check @PreAuthorize("hasRole('ADMIN')") void audit() { } } ``` 2. **Not a Spring bean.** If you `new OrderService()` yourself, there's no proxy at all. The object must be container-managed. 3. **Non-proxyable method.** `private`, `final`, or (for CGLIB) `final` classes can't be advised. Make the method `public` and non-final. 4. **Wrong/disabled flag.** Using `@Secured` without `securedEnabled = true`, or `@RolesAllowed` without `jsr250Enabled = true`. Unrecognized annotations are **ignored without error**. Also `@EnableMethodSecurity` might be missing entirely. 5. **Annotation on the wrong element.** Placing it on a private helper, or on a class Spring doesn't proxy. 6. **Interface vs. class placement mismatch** in odd JDK-proxy setups. ## Fixes for self-invocation - **Extract to another bean** and inject it — the cleanest option; the cross-bean call goes through that bean's proxy. - **Self-injection**: inject the bean into itself (`@Autowired OrderService self;`) and call `self.audit()`. - **`AopContext.currentProxy()`** with `@EnableMethodSecurity` / proxy config `exposeProxy = true`, then `((OrderService) AopContext.currentProxy()).audit()`. ## How to diagnose - Confirm `@EnableMethodSecurity` is present and the right flag is on for the annotation you used. - Check the bean is actually a proxy (`AopUtils.isAopProxy(bean)` / class name contains `$$SpringCGLIB$$`). - Ensure the call comes from *outside* the bean. - Ensure the method is `public` and the class/method non-`final`. ## Note This is a limitation of proxy-based AOP, identical to why `@Transactional` self-invocation doesn't start a transaction — same root cause. Switching to `mode = AdviceMode.ASPECTJ` (compile/load-time weaving) removes the self-invocation limitation but is rarely worth the setup cost.

  • Which other well-known Spring annotation suffers the exact same self-invocation limitation, and why?
    @Transactional — it is also implemented with AOP proxies, so an internal this.method() call bypasses the proxy and no transaction (or here, no security check) is applied. Same root cause.
  • You added @Secured but it does nothing. What's the first thing to check?
    Whether securedEnabled = true on @EnableMethodSecurity. If the flag is off, @Secured is an unrecognized annotation and is silently ignored with no error.

saying these in an interview costs you the question

  • Insisting AOP should catch self-invoked calls.
  • Adding @PreAuthorize to a private method and expecting it to work.
  • Not realizing a missing flag makes the annotation silently no-op.
  • Applying the annotation to a manually instantiated (non-bean) object.

context