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
3.9 KiB
← chapter index · next: the SpEL reference →
01 · How method security actually runs
Run: java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo1AnnotationsInAction
Output: output/demo1.txt
Source: Demo1AnnotationsInAction.java
The one-sentence model
@PreAuthorize is not a keyword the JVM understands. It is an annotation that a Spring AOP
advisor matches, on a proxy that wraps your bean, intercepting calls that arrive from
outside. Every trap in this repository follows from that sentence.
What @EnableMethodSecurity switches on
Nothing happens without it. Spring Boot's security auto-configuration does not enable method
security; the annotation is yours to add. Its attributes, read out of the AnnotationDefault
attributes in spring-security-config-7.1.1.jar rather than from documentation:
| Attribute | Default | Effect |
|---|---|---|
prePostEnabled |
true |
@PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter |
securedEnabled |
false |
@Secured |
jsr250Enabled |
false |
@RolesAllowed, @PermitAll, @DenyAll |
proxyTargetClass |
false |
Force CGLIB even when the bean implements interfaces |
mode |
AdviceMode.PROXY |
The alternative is ASPECTJ, which sidesteps chapters 03 and 04 entirely |
offset |
0 |
Shifts every security advisor's order by this amount |
Note the third row. @Secured("ROLE_ADMIN") and @RolesAllowed("ADMIN") compile, look
correct in review, and do nothing at all until you switch them on. That is the zeroth silent
failure, and unlike the two the article is named for it takes one attribute to fix.
There is no order attribute, despite what several guides say. It is offset, and it moves
all the interceptors together — see chapter 07.
The call path
caller
└─ proxy (CGLIB subclass, or JDK dynamic proxy)
└─ @PreFilter advisor, order 100 mutates the argument collection
└─ @PreAuthorize advisor, order 200 evaluates SpEL, throws or proceeds
└─ @PostAuthorize advisor, order 500
└─ @PostFilter advisor, order 600
└─ your method body
Each advisor builds a MethodSecurityExpressionRoot over the Authentication from
SecurityContextHolder plus the MethodInvocation, hands it to a
MethodSecurityExpressionHandler, and evaluates the annotation's expression against it.
What a denial looks like
Demo1 runs the same service as three identities. Two details from
output/demo1.txt are worth internalising:
- With an
Authenticationpresent but insufficient, you getAuthorizationDeniedException. - With no
Authenticationat all you getAuthenticationCredentialsNotFoundExceptioninstead — a different exception, from a different place, which an@ExceptionHandlerwritten only forAccessDeniedExceptionwill not catch.@PermitAllstill returns normally, because it never asks for theAuthentication.
In a servlet application both are normally translated by ExceptionTranslationFilter, so you
see 403 and 401 respectively. Outside a request — a scheduled job, a message listener, a test —
nothing translates them and they surface raw.
Where the Authentication comes from
SecurityContextHolder, on the calling thread. If the call happens on a thread that never
received the context, method security does not fail open; it throws
AuthenticationCredentialsNotFoundException. Getting the context onto that thread is a
separate topic with its own module in this repository — see
../context-propagation/.