In Spring Security 6, what actually enforces @PreAuthorize at runtime — walk through the interceptor and AuthorizationManager path and how it differs from the old model.
answer
- AuthorizationManagerBeforeMethodInterceptor (Pre) / AfterMethodInterceptor (Post)
- AuthorizationManager.check(Supplier<Auth>, MethodInvocation) -> AuthorizationDecision
- PreAuthorizeAuthorizationManager + MethodSecurityExpressionHandler
- replaces AccessDecisionManager + voters
- lazy Authentication supplier; AuthorizationDeniedEvent
basics
~20 sA method interceptor named AuthorizationManagerBeforeMethodInterceptor wraps the bean. Before the method runs it asks an AuthorizationManager (backed by a SpEL expression handler) to decide; if the decision is a deny, it throws AccessDeniedException. This replaces the old voter/AccessDecisionManager stack from @EnableGlobalMethodSecurity.
solid answer
~30 sWith @EnableMethodSecurity, Spring registers Advisor beans whose interceptors implement it. For @PreAuthorize it is `AuthorizationManagerBeforeMethodInterceptor` (a `MethodInterceptor` and `PointcutAdvisor`) configured with a `PreAuthorizeAuthorizationManager`; for @PostAuthorize it is `AuthorizationManagerAfterMethodInterceptor` with `PostAuthorizeAuthorizationManager`. Before (or after) the join point, the interceptor calls `AuthorizationManager.check(Supplier<Authentication>, MethodInvocation)`, which parses the SpEL via `MethodSecurityExpressionHandler`, evaluates it, and returns an `AuthorizationDecision`. A denial throws `AuthorizationDeniedException`/`AccessDeniedException` and publishes an `AuthorizationDeniedEvent`. This is the AuthorizationManager API introduced in Spring Security 5.8/6, replacing the legacy `AccessDecisionManager` + `AffirmativeBased` voters (`PreInvocationAuthorizationAdviceVoter`) used by the deprecated `@EnableGlobalMethodSecurity`. The interceptors are ordered so @PreAuthorize runs before @PostAuthorize.
code
java · 19 lines// Conceptual shape of what the interceptor does for @PreAuthorize
// (real class: AuthorizationManagerBeforeMethodInterceptor)
Object invoke(MethodInvocation mi) throws Throwable {
AuthorizationDecision decision =
authorizationManager.check(this::getAuthentication, mi); // PreAuthorizeAuthorizationManager
if (decision != null && !decision.isGranted()) {
eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision);
throw new AuthorizationDeniedException("Access Denied", decision);
}
return mi.proceed(); // run the real method
}
// Customizing the expression handler (e.g. add a RoleHierarchy or PermissionEvaluator)
@Bean
static MethodSecurityExpressionHandler expressionHandler(RoleHierarchy roleHierarchy) {
var handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}go deeper
Knowing 'a proxy/interceptor checks the rule and throws AccessDeniedException' is enough.
Name AuthorizationManagerBeforeMethodInterceptor and that it evaluates SpEL and throws on deny.
Trace interceptor -> AuthorizationManager.check -> AuthorizationDecision, and contrast with the legacy voter/AccessDecisionManager model.
Discuss composing AuthorizationManagers, custom expression handlers, event publishing, ordering, and migration strategy off @EnableGlobalMethodSecurity.
**The Spring Security 6 model.** When you add `@EnableMethodSecurity`, its `@Configuration` (`PrePostMethodSecurityConfiguration` and friends) registers a set of **Advisor** beans — one per supported annotation. Each Advisor pairs a **pointcut** (matches methods carrying the annotation) with a **MethodInterceptor** (the advice). For the two annotations in scope: - **@PreAuthorize →** `org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor`, configured with a `PreAuthorizeAuthorizationManager`. - **@PostAuthorize →** `AuthorizationManagerAfterMethodInterceptor`, configured with a `PostAuthorizeAuthorizationManager`. **The runtime path for @PreAuthorize:** 1. A call arrives *through the proxy*; Spring's interceptor chain reaches `AuthorizationManagerBeforeMethodInterceptor.invoke(MethodInvocation)`. 2. The interceptor calls `authorizationManager.check(authenticationSupplier, methodInvocation)`. The `Supplier<Authentication>` is lazy — the `Authentication` is pulled from the `SecurityContextHolder` only when the expression actually references it. 3. `PreAuthorizeAuthorizationManager` uses a `MethodSecurityExpressionHandler` (default `DefaultMethodSecurityExpressionHandler`) to build the SpEL evaluation context — creating the `MethodSecurityExpressionRoot`, binding method arguments (`#p0`, `#name`), `authentication`, and `principal` — then evaluates the parsed `@PreAuthorize` expression to a boolean. 4. It returns an `AuthorizationDecision` (`granted` true/false). If not granted, the interceptor throws `AuthorizationDeniedException` (a subclass of `AccessDeniedException`) and publishes an `AuthorizationDeniedEvent` through the `AuthorizationEventPublisher`. If granted, it proceeds to `invocation.proceed()` and the real method runs. **For @PostAuthorize** the shape is the same but the interceptor calls `proceed()` first, captures the returned object, exposes it as `returnObject`, and only then runs `PostAuthorizeAuthorizationManager`. On denial it discards the result and throws. **Ordering.** The interceptors carry explicit order values so that, when multiple annotations sit on one method, they run in the right sequence: `@PreFilter` → `@PreAuthorize` → (method) → `@PostAuthorize` → `@PostFilter`. `AuthorizationInterceptorsOrder` enumerates these. **What changed from the legacy model.** `@EnableGlobalMethodSecurity(prePostEnabled = true)` (Spring Security ≤5.x) used a completely different stack: a `MethodSecurityInterceptor` delegating to an `AccessDecisionManager` (usually `AffirmativeBased`) that polled a list of **voters**. `@PreAuthorize`/`@PostAuthorize` were handled by `PreInvocationAuthorizationAdviceVoter` / `PostInvocationAdviceProvider`, each voter returning `ACCESS_GRANTED / DENIED / ABSTAIN`, and the decision manager aggregating votes. The new `AuthorizationManager<T>` interface (`AuthorizationDecision check(Supplier<Authentication>, T)`) collapses that into a single, composable functional interface — you can wrap or compose managers (`AuthorizationManagers.allOf/anyOf`) far more easily than writing voters. It also makes the `Authentication` lookup lazy, avoiding needless `SecurityContext` reads when an expression is a constant like `permitAll`. **Customization hooks.** - Replace SpEL behavior by exposing a `MethodSecurityExpressionHandler` bean (e.g. to register a custom `PermissionEvaluator` or a `RoleHierarchy`). - Add cross-cutting behavior by publishing your own `AuthorizationManagerBeforeMethodInterceptor` with a custom `AuthorizationManager`, or wrap the default via `@EnableMethodSecurity(...)` plus interceptor beans. - Observe denials by supplying an `AuthorizationEventPublisher` and listening for `AuthorizationDeniedEvent`. **Scope note.** *How* the proxy is created and the interceptor chain is assembled is Spring AOP's concern; the security-relevant part is which interceptor/AuthorizationManager evaluates the rule and what it throws.
- Why is the Authentication passed as a Supplier rather than a resolved object?To make the lookup lazy — the SecurityContext is only read if the SpEL actually references authentication/principal. Constant rules like permitAll or denyAll never touch the context, saving work.
- How do you plug a custom PermissionEvaluator into hasPermission()?Expose a static @Bean MethodSecurityExpressionHandler (DefaultMethodSecurityExpressionHandler) and call setPermissionEvaluator(...) on it; the Pre/PostAuthorize managers pick it up when building the SpEL context.
saying these in an interview costs you the question
- Claiming Spring Security 6 still uses AccessDecisionManager and voters for @PreAuthorize (that's the deprecated pre-6 stack)
- Saying the Authentication is eagerly resolved for every call regardless of the expression
- Confusing the interceptor with a servlet Filter — method security is AOP, not the filter chain