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

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 @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 has the patterns.

← 03 · self-invocation · chapter index · next: filtering →