skip to content

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.

level: seniorimportance: should knowfreq 42%

answer

  1. AuthorizationManagerBeforeMethodInterceptor (Pre) / AfterMethodInterceptor (Post)
  2. AuthorizationManager.check(Supplier<Auth>, MethodInvocation) -> AuthorizationDecision
  3. PreAuthorizeAuthorizationManager + MethodSecurityExpressionHandler
  4. replaces AccessDecisionManager + voters
  5. lazy Authentication supplier; AuthorizationDeniedEvent

basics

~20 s

A 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 s

With @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
java
// 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

for a junior

Knowing 'a proxy/interceptor checks the rule and throws AccessDeniedException' is enough.

for a middle

Name AuthorizationManagerBeforeMethodInterceptor and that it evaluates SpEL and throws on deny.

for a senior

Trace interceptor -> AuthorizationManager.check -> AuthorizationDecision, and contrast with the legacy voter/AccessDecisionManager model.

for a principal

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

context