skip to content

PermissionEvaluator & ACLs

hasPermission() delegates to a PermissionEvaluator, which for domain-object security can be backed by the ACL module and its permission tables. The right answer when authorization depends on a specific record rather than a role.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

4

How do you implement and register a custom PermissionEvaluator in Spring Security 6?

level: middleimportance: must knowfreq 55%

answer

  1. implement both hasPermission overloads
  2. return false, never throw
  3. DefaultMethodSecurityExpressionHandler.setPermissionEvaluator
  4. @Bean must be static → avoid premature init
  5. runs per call / per filtered element

basics

~10 s

Implement the PermissionEvaluator interface (its two hasPermission methods) with your own logic, then publish a MethodSecurityExpressionHandler bean, calling setPermissionEvaluator() so @PreAuthorize's hasPermission() uses it.

solid answer

~40 s

Write a class implementing PermissionEvaluator's two overloads: hasPermission(auth, targetObject, permission) and hasPermission(auth, id, type, permission). Inside, cast target/permission to your domain types and return your ownership/ACL decision. To wire it, expose a MethodSecurityExpressionHandler bean: create a DefaultMethodSecurityExpressionHandler, call setPermissionEvaluator(yourEvaluator), and return it. Make the bean method static so it's built early and doesn't trigger premature initialization of security infrastructure or other beans. With @EnableMethodSecurity present, Spring picks up this custom expression handler, and every hasPermission() in your @PreAuthorize/@PostAuthorize now flows through your evaluator. The id-based overload lets you authorize without loading the object; a common pattern is to load minimal ownership info from the DB and compare against authentication.getName().

code

java · 27 lines
java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {

    // static → built before the @Configuration instance,
    // avoids premature-initialization of security infra
    @Bean
    static MethodSecurityExpressionHandler methodSecurityExpressionHandler(
            PermissionEvaluator evaluator) {
        DefaultMethodSecurityExpressionHandler handler =
                new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(evaluator);
        return handler;
    }
}

@Component
class DocumentPermissionEvaluator implements PermissionEvaluator {
    @Override public boolean hasPermission(Authentication a, Object t, Object p) {
        return a != null && t instanceof Document d
                && d.getOwner().equals(a.getName());
    }
    @Override public boolean hasPermission(Authentication a, Serializable id,
                                           String type, Object p) {
        return false; // delegate to a repository lookup in real code
    }
}

go deeper

for a junior

Know the interface exists and that you register it somewhere for hasPermission to work.

for a middle

Implement both overloads, register via DefaultMethodSecurityExpressionHandler, know the static-bean rule.

for a senior

Explain premature-init, composite delegation, per-element cost under @PreFilter/@PostFilter.

for a principal

Reason about where the check belongs (evaluator vs service), caching, and consistency of the permission vocabulary across the codebase.

## Step 1 — implement the SPI `PermissionEvaluator` has **two methods**, both of which you must implement (delegate one to the other if you only support one style): ```java public class DocumentPermissionEvaluator implements PermissionEvaluator { private final DocumentRepository repo; public DocumentPermissionEvaluator(DocumentRepository repo) { this.repo = repo; } @Override public boolean hasPermission(Authentication auth, Object target, Object permission) { if (auth == null || !(target instanceof Document doc)) return false; return check(doc, auth.getName(), permission.toString()); } @Override public boolean hasPermission(Authentication auth, Serializable id, String type, Object permission) { if (auth == null || id == null) return false; Document doc = repo.findById((Long) id).orElse(null); if (doc == null) return false; return check(doc, auth.getName(), permission.toString()); } private boolean check(Document doc, String user, String perm) { if ("READ".equals(perm)) return doc.isPublic() || doc.getOwner().equals(user); if ("WRITE".equals(perm)) return doc.getOwner().equals(user); return false; } } ``` Key points: **return false, never throw**, for the 'not allowed' case (an exception becomes a 500, not a clean 403). Guard against `null` authentication and wrong `instanceof` types. The `permission` object is whatever the SpEL passed — commonly a `String`. ## Step 2 — register it via the expression handler Spring builds SpEL for method security through a **`MethodSecurityExpressionHandler`** (default impl `DefaultMethodSecurityExpressionHandler`). You override the bean and inject your evaluator: ```java @Configuration @EnableMethodSecurity public class MethodSecurityConfig { @Bean static MethodSecurityExpressionHandler expressionHandler(DocumentPermissionEvaluator evaluator) { var handler = new DefaultMethodSecurityExpressionHandler(); handler.setPermissionEvaluator(evaluator); return handler; } } ``` ### Why `static`? `@EnableMethodSecurity` registers `BeanPostProcessor`-adjacent infrastructure very early. A **non-static** `@Bean` method lives on a `@Configuration` instance that must be fully constructed (and CGLIB-proxied) before the method runs; if the security infrastructure needs the expression handler before that, you get premature-initialization warnings or subtle ordering bugs. Marking the bean method **static** lets Spring invoke it without instantiating the enclosing config, sidestepping the cycle. This is the officially recommended pattern for security beans that feed the method-security setup. ## Step 3 — use it Any `@PreAuthorize("hasPermission(#doc,'WRITE')")` now dispatches to your evaluator. Nothing else changes. ## Gotchas - **Only one PermissionEvaluator** is held by the handler; to combine strategies, write a composite that delegates by target type. - **`@PostAuthorize`** runs the method first, so a denied `hasPermission(returnObject,…)` still executed the query/side-effects — fine for reads, dangerous for mutations. - The evaluator must be **fast**: it runs on every guarded call. Cache or use the id-overload to avoid redundant loads. - If you also use `@PreFilter`/`@PostFilter` with `hasPermission`, the evaluator is called **once per element** — watch N+1 loads. - Prefer returning a plain `boolean`; do not rely on throwing `AccessDeniedException` yourself — the framework raises it when the expression is false.

  • Why is the MethodSecurityExpressionHandler @Bean method usually declared static?
    So Spring can invoke it without fully constructing the @Configuration bean. Method-security infrastructure is registered very early; a non-static bean would force premature initialization of the config class (and anything it depends on), producing ordering warnings or cycles.
  • Your evaluator throws an exception for the deny case — what's the user-visible effect?
    A 500, not a 403. The framework interprets a false return as access-denied and raises AccessDeniedException itself; throwing your own runtime exception escapes as a server error. Return false for denial.
  • How would you support multiple domain types with different rules?
    Write a composite PermissionEvaluator that inspects the target's type (or the type string in the id-overload) and delegates to a per-type evaluator, since the handler holds only one PermissionEvaluator.

saying these in an interview costs you the question

  • Registering the evaluator with a non-static @Bean and hitting premature-initialization issues without understanding why.
  • Throwing an exception instead of returning false for the denied case.
  • Only implementing one of the two hasPermission overloads and being surprised the id-based SpEL form fails.
  • Ignoring that @PostAuthorize executes the method before the check, so side effects already happened.

context

open as a page

What does the hasPermission() expression do inside @PreAuthorize, and what backs it?

level: juniorimportance: should knowfreq 45%

basics

~10 s

hasPermission() is a SpEL check you write in @PreAuthorize/@PostAuthorize. It asks 'can this user do X on this object?'. Spring delegates the yes/no answer to a PermissionEvaluator bean you configure.

open as a page

How does the spring-security-acl module implement domain-object security, and what are its moving parts?

level: seniorimportance: should knowfreq 35%

basics

~10 s

spring-security-acl stores per-object, per-user permissions in four DB tables. AclPermissionEvaluator (a PermissionEvaluator) looks up an object's ACL via AclService and checks whether the current user's Sid was granted the requested Permission mask.

open as a page

When would you choose spring-security-acl over a custom PermissionEvaluator or a query-level filter, and what are the pitfalls at scale?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Use spring-security-acl only when permissions are per-object, admin-managed, and change at runtime (Google-Docs-style sharing). For ownership or role rules, a small custom PermissionEvaluator — or filtering in the SQL query — is simpler and faster.

open as a page