A @PreAuthorize annotation on a service method is being ignored at runtime. What are the likely causes given how @EnableMethodSecurity works?
answer
- Proxy boundary — call must cross it
- Self-invocation this.x() bypasses (like @Transactional)
- Not a bean / private / final = no advice
- Missing flag → annotation silently ignored
- Fix: separate bean, self-inject, or AopContext.currentProxy()
basics
~20 sMethod 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@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
Know the check only runs when the method is called from outside the bean.
List the proxy-related causes (self-invocation, non-bean, private/final, missing flag) and give fixes.
Connect it to the shared proxy limitation with @Transactional and choose the cleanest structural fix.
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.