1
0
Files
spring-security-demo/method-security/docs/01-how-method-security-runs.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

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 Authentication present but insufficient, you get AuthorizationDeniedException.
  • With no Authentication at all you get AuthenticationCredentialsNotFoundException instead — a different exception, from a different place, which an @ExceptionHandler written only for AccessDeniedException will not catch. @PermitAll still returns normally, because it never asks for the Authentication.

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/.

← chapter index · next: the SpEL reference →