0Pricing
Spring Boot 4 Complete Guide · Lesson

Method Security with SpEL and Custom Voters

Enforce fine-grained access using @PreAuthorize, SpEL expressions, and custom authorization logic.

Method Security with SpEL and Custom Voters is a free Spring Boot 4 Complete Guide lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Spring Boot 4 Complete Guide learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Method Security?

URL-based security (HttpSecurity matchers) guards entry points, but it cannot see the arguments a method receives or the object it returns. Method security closes that gap by enforcing rules right at the service layer.

  • Defense in depth — protection survives even if a controller forgets a check.
  • Fine-grained — decide based on parameters, return values, and the authenticated principal.
  • Reusable — the same secured service can be called from REST, GraphQL, or a message listener and stays protected.

In this lesson we enforce access with @PreAuthorize, SpEL expressions, and a custom authorization manager.

Enabling Method Security

In Spring Boot 4 / Spring Security 6, method security is opt-in. Add @EnableMethodSecurity to a configuration class. It activates the annotations through an AOP proxy.

  • prePostEnabled defaults to true@PreAuthorize and @PostAuthorize work out of the box.
  • Set securedEnabled = true for the legacy @Secured, or jsr250Enabled = true for @RolesAllowed.

Note: the old @EnableGlobalMethodSecurity is removed — always use @EnableMethodSecurity.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // prePostEnabled = true by default
    // @PreAuthorize / @PostAuthorize now active
}

@PreAuthorize with Roles and Authorities

@PreAuthorize evaluates a SpEL expression before the method runs. If it returns false, Spring throws AccessDeniedException and the body never executes.

  • hasRole('ADMIN') — checks the ROLE_ADMIN authority (the prefix is added for you).
  • hasAuthority('SCOPE_orders:write') — exact authority match, no prefix added.
  • hasAnyRole('ADMIN','MANAGER') and the boolean operators and / or / !.
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class AccountService {

    @PreAuthorize("hasRole('ADMIN')")
    public void closeAccount(Long accountId) {
        // only ROLE_ADMIN reaches here
    }

    @PreAuthorize("hasAnyRole('ADMIN','SUPPORT') or hasAuthority('SCOPE_accounts:write')")
    public void freezeAccount(Long accountId) {
        // ...
    }
}

Referencing Method Arguments with #

The real power of SpEL is reading method arguments. Prefix a parameter name with # to use it inside the expression. This lets you compare the principal's identity to the data being acted on.

  • authentication — the current Authentication object.
  • principal — the principal (often a UserDetails or JWT).
  • #username, #order.ownerId — method arguments and their properties.

Argument names require parameters compiled with -parameters (Spring Boot enables this by default).

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class ProfileService {

    // A user may edit only their own profile, unless they are an admin
    @PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
    public void updateProfile(String username, ProfileDto dto) {
        // ...
    }
}

@PostAuthorize and returnObject

@PostAuthorize runs after the method returns and can inspect the result via returnObject. Use it when you must load the entity first to know who owns it.

  • Good for “you can read this record only if it belongs to you.”
  • The method body does execute, so avoid it for operations with side effects you must prevent.
  • On denial, the return value is discarded and AccessDeniedException is thrown.
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

    @PostAuthorize("returnObject.ownerUsername == authentication.name or hasRole('ADMIN')")
    public Document findById(Long id) {
        return repository.findById(id).orElseThrow();
    }
}

@PreFilter and @PostFilter on Collections

Filtering annotations prune collections element by element instead of throwing. They use a special variable filterObject bound to each element.

  • @PreFilter — strips disallowed elements from a collection argument before the method runs.
  • @PostFilter — strips disallowed elements from the returned collection.
  • Use filterTarget when a method has more than one collection parameter.

Caution: post-filtering large result sets in memory can be costly — prefer filtering in the query when possible.

import org.springframework.security.access.prepost.PostFilter;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class OrderService {

    // Caller sees only the orders they own (admins see all)
    @PostFilter("filterObject.ownerUsername == authentication.name or hasRole('ADMIN')")
    public List<Order> findRecentOrders() {
        return repository.findRecent();
    }
}

Calling a Bean from SpEL with @

When logic gets complex, push it into a Spring bean and call it from the expression using @beanName.method(...). This keeps annotations readable and the rule unit-testable.

  • The @ resolves a bean from the application context.
  • Pass authentication and method arguments straight into the bean method.
  • The method must return a boolean.
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

@Component("projectAccess")
public class ProjectAccessEvaluator {

    public boolean canEdit(Authentication auth, Long projectId) {
        String user = auth.getName();
        return membershipRepository.isEditor(user, projectId);
    }
}

// Usage on a service method:
// @PreAuthorize("@projectAccess.canEdit(authentication, #projectId)")
// public void rename(Long projectId, String name) { ... }

PermissionEvaluator and hasPermission

SpEL exposes hasPermission(target, permission), which delegates to a PermissionEvaluator bean. It is the canonical hook for domain-object (ACL-style) authorization without inlining logic in every annotation.

  • hasPermission(#doc, 'WRITE') — passes the object and a permission key.
  • hasPermission(#id, 'com.app.Document', 'READ') — passes an id plus the type.
  • You register exactly one PermissionEvaluator via a MethodSecurityExpressionHandler.
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import java.io.Serializable;

public class DocumentPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication auth, Object target, Object permission) {
        if (target instanceof Document doc) {
            return "WRITE".equals(permission)
                ? doc.getOwnerUsername().equals(auth.getName())
                : true; // READ allowed for all in this example
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication auth, Serializable id,
                                 String type, Object permission) {
        return false; // resolve by id+type if needed
    }
}

Registering a Custom Expression Handler

To wire your PermissionEvaluator into SpEL, expose a DefaultMethodSecurityExpressionHandler bean and set the evaluator on it. Spring Security picks it up for all method annotations.

  • The bean name is not important; the type is.
  • You can also attach a custom RoleHierarchy here so hasRole respects inheritance.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity
public class ExpressionHandlerConfig {

    @Bean
    static DefaultMethodSecurityExpressionHandler expressionHandler() {
        var handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(new DocumentPermissionEvaluator());
        return handler;
    }
}

Custom AuthorizationManager (the New Voter)

Spring Security 6 replaced the legacy AccessDecisionVoter with the simpler AuthorizationManager<T>. For method security the type parameter is MethodInvocation. Implement check to return an AuthorizationDecision.

  • Return new AuthorizationDecision(true|false) — or null to abstain and let other managers decide.
  • Register it with @EnableMethodSecurity(prePostEnabled = false) plus an advisor, or combine managers with AuthorizationManagers.allOf(...).
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import java.util.function.Supplier;

public class BusinessHoursAuthorizationManager
        implements AuthorizationManager<MethodInvocation> {

    @Override
    public AuthorizationDecision check(Supplier<Authentication> auth,
                                       MethodInvocation invocation) {
        int hour = java.time.LocalTime.now().getHour();
        boolean withinHours = hour >= 9 && hour < 18;
        return new AuthorizationDecision(withinHours);
    }
}

Pre vs Post: Choosing Correctly

Picking the wrong annotation either leaks data or blocks valid calls. A quick decision guide:

  • Rule depends only on arguments + principal@PreAuthorize (fast, no side effects).
  • Rule depends on the loaded entity's ownership → @PostAuthorize.
  • Trimming a collection per-element → @PostFilter (or filter in the query).
  • Reusable domain-object rule → hasPermission + PermissionEvaluator.

Remember: @PostAuthorize and @PostFilter run the method body, so never rely on them to stop a mutating operation.

Quick Check

You have a method Document findById(Long id) that loads a document, and access should be granted only if the returned document's ownerUsername equals the caller, or the caller is an admin. Which annotation expresses this correctly?

Recap

You now enforce fine-grained access at the method layer:

  • @EnableMethodSecurity turns on annotation-driven checks (no more @EnableGlobalMethodSecurity).
  • @PreAuthorize guards before execution using SpEL: hasRole, hasAuthority, #args, and authentication.
  • @PostAuthorize inspects returnObject; @PreFilter/@PostFilter prune collections via filterObject.
  • Push complex rules into a bean (@beanName.method(...)) or a PermissionEvaluator behind hasPermission.
  • For cross-cutting policy, implement AuthorizationManager<MethodInvocation> — the modern replacement for voters.

Rule of thumb: prefer pre-checks for speed and safety; reach for post-checks only when the decision needs the loaded data.

Frequently asked questions

Is the “Method Security with SpEL and Custom Voters” lesson free?

Yes — the full text of “Method Security with SpEL and Custom Voters” is free to read here on the web, and the Spring Boot 4 Complete Guide course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Spring Boot 4 Complete Guide course, upgrade to CoddyKit PRO.

What will I learn in “Method Security with SpEL and Custom Voters”?

Enforce fine-grained access using @PreAuthorize, SpEL expressions, and custom authorization logic. You practise Spring Boot 4 Complete Guide with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Spring Boot 4 Complete Guide?

No prior experience is required. Spring Boot 4 Complete Guide on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Method Security with SpEL and Custom Voters” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Spring Boot 4 Complete Guide lesson?

Yes. Every Spring Boot 4 Complete Guide lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Resource Server JWT Validation and Claims
  2. OAuth2 Client and Authorization Code Flow
  3. Method Security with SpEL and Custom Voters
  4. Opaque Token Introspection and Token Exchange
← Back to Spring Boot 4 Complete Guide