skip to content

What variables can you reference in @PreAuthorize/@PostAuthorize SpEL — how do you read method arguments, the principal, and the return value?

level: middleimportance: must knowfreq 72%

answer

  1. root = MethodSecurityExpressionRoot
  2. #p0/#a0 positional, #name by parameter name
  3. authentication + principal (no #)
  4. returnObject only in @PostAuthorize
  5. @bean.method(...) for custom logic

basics

~20 s

You can reference method arguments by name (or #p0, #p1), the current user via authentication and principal, and — only in @PostAuthorize — the method's result via returnObject. You combine them with helpers like hasRole() and hasAuthority().

solid answer

~30 s

The SpEL root object is a MethodSecurityExpressionOperations, so you get built-in helpers (hasRole, hasAuthority, isAuthenticated, permitAll, denyAll) plus two objects: `authentication` (the full Authentication) and `principal` (its principal, usually a UserDetails). Method arguments are exposed as SpEL variables: by parameter name (e.g. `#userId`) when parameter names are compiled in or `@P`/Spring's parameter-name discovery is available, and positionally as `#p0`, `#p1`, or `#a0`. In `@PostAuthorize` only, `returnObject` holds the value the method returned. Typical ownership check: `@PostAuthorize("returnObject.ownerId == principal.id")`, or a pre-check like `@PreAuthorize("#userId == principal.id or hasRole('ADMIN')")`. `filterObject` exists too, but only inside @PreFilter/@PostFilter, not pre/post-authorize.

code

java · 19 lines
java
@Service
public class AccountService {

    // argument by name + principal + role fallback
    @PreAuthorize("#accountId == principal.id or hasRole('ADMIN')")
    public Account get(Long accountId) { ... }

    // positional arg alias when names aren't reliable
    @PreAuthorize("#p0 > 0")
    public void withdraw(long amount) { ... }

    // decision based on the returned object
    @PostAuthorize("returnObject.ownerName == authentication.name")
    public Document load(Long id) { return repo.findById(id).orElseThrow(); }

    // delegating to a custom bean for complex rules
    @PreAuthorize("@permissions.canEdit(#docId, authentication)")
    public void edit(Long docId) { ... }
}

go deeper

for a junior

Know that arguments are referenced with #, and hasRole/authentication exist.

for a middle

Distinguish #name vs #p0, know authentication/principal, and that returnObject is @PostAuthorize-only.

for a senior

Explain parameter-name discovery requirements, the @bean delegation pattern, and anonymous-principal pitfalls.

for a principal

Weigh SpEL-injection risk, testability of externalized expression beans, and consistency of principal types across JWT/UserDetails.

When Spring evaluates a `@PreAuthorize`/`@PostAuthorize` expression, the **root object** is an instance of `MethodSecurityExpressionOperations` (concretely `MethodSecurityExpressionRoot`). That root exposes both helper methods and named data you can reference. **Built-in helper methods (called with no `#`):** - `hasRole('ADMIN')` — true if the user has authority `ROLE_ADMIN` (the `ROLE_` prefix is added automatically). - `hasAuthority('SCOPE_read')` — exact authority match, no prefix magic. - `hasAnyRole(...)`, `hasAnyAuthority(...)`. - `isAuthenticated()`, `isFullyAuthenticated()`, `isAnonymous()`, `isRememberMe()`. - `permitAll`, `denyAll` (properties, always true/false). - `hasPermission(...)` — delegates to a configured `PermissionEvaluator`. **Named objects (no `#` prefix — they are properties of the root):** - `authentication` — the full `org.springframework.security.core.Authentication`. Use `authentication.name`, `authentication.authorities`. - `principal` — `authentication.getPrincipal()`, commonly your `UserDetails` implementation or a `Jwt`/`OAuth2User`. So `principal.username` or a custom `principal.id` works if your principal type has that property. **Method arguments (referenced with `#`):** - **By name:** `#userId` matches the parameter named `userId`. This requires parameter names to be discoverable — either compile with `-parameters` (Spring Boot enables this), or annotate the parameter with `@org.springframework.security.access.method.P("userId")`, or use `@PreAuthorize` with a custom parameter-name discoverer. Kotlin retains parameter names, so `#userId` works out of the box. - **By position:** `#p0`, `#p1`, … or the aliases `#a0`, `#a1`, … always work regardless of name availability. **Return value (only in @PostAuthorize):** - `returnObject` — the object the method returned. `@PostAuthorize("returnObject.ownerId == principal.id")` is the canonical ownership-after-fetch pattern. If the method returns `void`/`Optional`/collection, `returnObject` reflects exactly that value (an empty `Optional` is still an object, not null). **What is NOT available here:** `filterObject` (the current element being filtered) exists only in `@PreFilter`/`@PostFilter`. Trying to use it in pre/post-authorize will not resolve. **Gotchas:** - `hasRole('ROLE_ADMIN')` double-prefixes to `ROLE_ROLE_ADMIN` and silently fails — pass `hasRole('ADMIN')`. - If parameter names are stripped (older builds without `-parameters`), `#userId` resolves to nothing and the expression may throw or evaluate unexpectedly — prefer `#p0` when unsure or annotate with `@P`. - SpEL runs with the privileges of your app; do not build expressions from untrusted input (SpEL injection risk). - `principal` can be a `String` (`"anonymousUser"`) for anonymous requests, so `principal.id` would fail — guard with `isAuthenticated()` first. **Custom expressions** — you can register a bean and call it: `@PreAuthorize("@authz.canEdit(#docId, authentication)")`, where `authz` is a Spring bean name. This keeps complex logic in testable Java instead of long SpEL strings.

  • Why might #userId fail to resolve while #p0 works?
    #userId needs parameter names at runtime; without -parameters (or @P/Kotlin metadata) names are erased, so name-based references don't bind while positional #p0/#a0 always do.
  • Can you call a Spring bean method from inside a @PreAuthorize expression?
    Yes — the @beanName syntax resolves a bean from the context, e.g. @PreAuthorize("@authz.canEdit(#id, authentication)"). It keeps complex, testable logic out of the SpEL string.

saying these in an interview costs you the question

  • Using returnObject in @PreAuthorize (it only exists in @PostAuthorize)
  • Writing hasRole('ROLE_ADMIN') which double-prefixes to ROLE_ROLE_ADMIN
  • Referencing principal.id for anonymous users where principal is the String 'anonymousUser'
  • Confusing filterObject (Pre/PostFilter) with returnObject (PostAuthorize)

context