Moves the existing virtual-thread/context-propagation project into context-propagation/ and adds method-security/ for the Spring Security 7 method-security article: nine runnable demos, fourteen assertions, and every transcript the article quotes, regenerated by scripts/run-all.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
3.5 KiB
← 02 · SpEL reference · chapter index · next: non-proxyable methods →
03 · Self-invocation
Run: java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo2SelfInvocation
Output: output/demo2.txt
Source: Demo2SelfInvocation.java
The shape
@PreAuthorize("hasRole('ADMIN')")
public String adminReport() { return "TOP SECRET REVENUE NUMBERS"; }
public String userEntryPoint() {
return adminReport(); // no proxy involved
}
A caller invoking adminReport() from outside goes through the proxy and is denied. A caller
invoking userEntryPoint() reaches the target object, and this.adminReport() is a plain
virtual call on the target. The advisor never sees it.
From output/demo2.txt, as a user holding only ROLE_USER:
reports.adminReport() (via proxy) DENIED -> AuthorizationDeniedException
reports.userEntryPoint() (this.adminReport()) ALLOWED -> TOP SECRET REVENUE NUMBERS
Why it survives review
The same run prints:
ReportService.adminReport() @PreAuthorize -> @PreAuthorize("hasRole('ADMIN')")
bean is an AOP proxy -> true
proxy class -> ...ReportService$$SpringCGLIB$$0
The annotation is present. The bean is proxied. Reflection agrees with the source. Every individual check a reviewer would run passes; only the composition is wrong. Nothing logs at any level, no metric moves, and the method returns the right answer to the wrong person.
Three fixes, in order of preference
Move the method to a different bean. A call between two beans is an external call by definition. This is the boring answer and it is usually the right one, because a method that needs its own authorization rule is usually a different responsibility.
Inject the bean into itself through an ObjectProvider.
private final ObjectProvider<ReportService> self;
public String viaSelfInjection() {
return this.self.getObject().adminReport();
}
A direct field of the bean's own type is a circular reference the container refuses in a
constructor; ObjectProvider resolves lazily at call time and hands back the proxy.
AopContext.currentProxy().
public String viaAopContext() {
return ((ReportService) AopContext.currentProxy()).adminReport();
}
This needs @EnableAspectJAutoProxy(exposeProxy = true) — without it the call throws
IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available. It also couples the method to the fact that it is proxied, which
is why it is last on this list.
All three are verified in the same run; all three produce AuthorizationDeniedException.
The structural fix
@EnableMethodSecurity(mode = AdviceMode.ASPECTJ) weaves the advice into the bytecode
instead of wrapping the object, and self-invocation stops being a thing. It also requires
AspectJ weaving in the build, which is a large change to make for one class of bug. Consider it
if you are already weaving.
The cheaper structural answer is not to depend on method security alone: keep a catch-all
authorizeHttpRequests rule (anyRequest().authenticated()) so that an inner method that
escapes its own check is still behind a request-level one.
← 02 · SpEL reference · chapter index · next: non-proxyable methods →