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
4.1 KiB
← 03 · self-invocation · chapter index · next: filtering →
04 · Methods the proxy cannot advise
Run: java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo3NonProxyable
Output: output/demo3.txt
Source: 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, 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), 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.
What to actually do
- Do not put
@PreAuthorizeon anythingprivateorstatic. Neither is reachable through a proxy, and neither will tell you. - Remove
finalfrom 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 has the patterns.