1
0
Files
spring-security-demo/method-security/docs/04-non-proxyable-methods.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

86 lines
4.1 KiB
Markdown

[← 03 · self-invocation](03-self-invocation.md) · [chapter index](README.md) · [next: filtering →](05-filtering.md)
# 04 · Methods the proxy cannot advise
Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo3NonProxyable`
Output: [`output/demo3.txt`](output/demo3.txt)
Source: [`Demo3NonProxyable.java`](../src/main/java/com/ankurm/methodsec/Demo3NonProxyable.java)
A CGLIB proxy is a generated subclass. It intercepts a method by overriding it. Anything that
cannot be overridden cannot be advised.
## The results
Same class, same `@PreAuthorize("hasRole('ADMIN')")` on every method, same `ROLE_USER` caller:
| Declaration | Advised? | How you find out |
|---|---|---|
| `public` | yes | denied, as intended |
| `public final` | **no** | a `WARNING` from `CglibAopProxy` at startup |
| `static` | **no** | nothing at all |
| package-private | yes | denied — the generated subclass is in the same package |
| `private` | **no** | nothing at all; also unreachable except by self-invocation |
| `final class` | n/a | the context **fails to start** |
The package-private row is the surprise. Received wisdom is "only public methods are secured";
the generated subclass lands in the same package as the target, so it can and does override a
package-private method. Verified by reflection in the same run:
```
publicAdminOnly declared final=false overridden by proxy=true
finalAdminOnly declared final=true overridden by proxy=false
packagePrivateAdminOnly declared final=false overridden by proxy=true
```
## `final` is the loud one, `static` and `private` are not
Spring does warn about a public `final` method — `CglibAopProxy.doValidateClass` logs:
```
WARNING: Public final method [public final java.lang.String ...Vault.finalAdminOnly()]
cannot get proxied via CGLIB, consider removing the final marker or using interface-based
JDK proxies.
```
It is in [`output/demo3.txt`](output/demo3.txt), at the very top, at startup, mixed in with
everything else an application logs while booting. It is easy to miss and it is more than you
get for `static` and `private`, which produce nothing.
A `final` **class** is different again: CGLIB cannot subclass it at all, so the container
refuses to start with
`IllegalArgumentException: Cannot subclass final class ...SealedVault`. That is the friendliest
failure in this chapter. It is also the reason a Java `record` cannot carry method security on
its own methods — records are final. If you need a secured getter on a returned object (see
[chapter 06](06-denied-handling.md)), it has to be a non-final class.
## Interfaces and JDK proxies
`@EnableMethodSecurity(proxyTargetClass = false)` is the default, so a bean that implements an
interface gets a **JDK dynamic proxy**, which implements only the interfaces. A public
annotated method that is not on the interface is then not merely unadvised — it is not on the
proxy at all:
```
proxy is a JDK proxy -> true
proxied interfaces -> [interface ...LedgerOperations]
cast proxy to Ledger impl -> ClassCastException: class jdk.proxy2.$Proxy18 cannot be cast to ...Ledger
```
In a Spring Boot application this is usually moot: Boot sets `proxyTargetClass = true` globally
via `spring.aop.proxy-target-class`, which defaults to `true`. In a plain Spring context, or
with that property flipped, it is live. Either way, the `ClassCastException` is loud — the
dangerous version is the one where the interface method *is* annotated and the implementation
carries a second, different annotation; see [chapter 08](08-meta-annotations.md).
## What to actually do
- Do not put `@PreAuthorize` on anything `private` or `static`. Neither is reachable through a
proxy, and neither will tell you.
- Remove `final` from methods that carry security annotations, or make peace with the fact that
the annotation is documentation.
- If you use records or other final classes as return values, secure the method that returns
them, not the accessors on them.
- Grep for the combination. [Chapter 09](09-audit-checklist.md) has the patterns.
[← 03 · self-invocation](03-self-invocation.md) · [chapter index](README.md) · [next: filtering →](05-filtering.md)