1
0
Files
spring-security-demo/method-security/docs/03-self-invocation.md
asmhatre 5e9e7f1b12 Split into per-article modules and add the method-security module
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
2026-08-25 02:01:29 +00:00

91 lines
3.5 KiB
Markdown

[← 02 · SpEL reference](02-spel-reference.md) · [chapter index](README.md) · [next: non-proxyable methods →](04-non-proxyable-methods.md)
# 03 · Self-invocation
Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo2SelfInvocation`
Output: [`output/demo2.txt`](output/demo2.txt)
Source: [`Demo2SelfInvocation.java`](../src/main/java/com/ankurm/methodsec/Demo2SelfInvocation.java)
## The shape
```java
@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`](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`.**
```java
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()`.**
```java
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](02-spel-reference.md) · [chapter index](README.md) · [next: non-proxyable methods →](04-non-proxyable-methods.md)