skip to content

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