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
This commit is contained in:
85
method-security/docs/04-non-proxyable-methods.md
Normal file
85
method-security/docs/04-non-proxyable-methods.md
Normal file
@@ -0,0 +1,85 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user