What variables can you reference in @PreAuthorize/@PostAuthorize SpEL — how do you read method arguments, the principal, and the return value?
answer
- root = MethodSecurityExpressionRoot
- #p0/#a0 positional, #name by parameter name
- authentication + principal (no #)
- returnObject only in @PostAuthorize
- @bean.method(...) for custom logic
basics
~20 sYou 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 sThe 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@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
Know that arguments are referenced with #, and hasRole/authentication exist.
Distinguish #name vs #p0, know authentication/principal, and that returnObject is @PostAuthorize-only.
Explain parameter-name discovery requirements, the @bean delegation pattern, and anonymous-principal pitfalls.
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)