Skip to content
Spring Boot sb security 3 min read

Method-Level Security

URL-based rules in authorizeHttpRequests protect endpoints, but they can’t easily express rules that depend on method arguments or the returned object — “a user may edit only their own profile,” for example. Method-level security moves authorization onto the service layer using annotations evaluated with SpEL (Spring Expression Language), giving you fine-grained, reusable checks close to the business logic.

Enabling method security

Add @EnableMethodSecurity to a @Configuration class. In Spring Security 6 this annotation enables @PreAuthorize/@PostAuthorize by default; opt in to the legacy annotations explicitly.

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

@Configuration
@EnableMethodSecurity(
        prePostEnabled = true,   // @PreAuthorize / @PostAuthorize (default true)
        securedEnabled = true)   // @Secured (default false)
public class MethodSecurityConfig { }

Note: @EnableMethodSecurity replaces the old @EnableGlobalMethodSecurity. It uses Spring’s standard AOP (CGLIB/JDK proxies) under the hood.

@PreAuthorize

@PreAuthorize runs before the method, rejecting the call with AccessDeniedException if the SpEL expression is false. This is the workhorse.

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

@Service
public class AccountService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteAccount(Long id) { ... }

    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public Account view(Long id) { ... }

    // Argument-based: a user may update only their own account
    @PreAuthorize("#id == authentication.principal.id")
    public void update(Long id, AccountForm form) { ... }

    // Combine conditions
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public void reset(Long id) { ... }
}

For #id == authentication.principal.id to work, the principal must be your UserDetails implementation exposing an id getter — for instance the UserAccount entity from database authentication.

@PostAuthorize

@PostAuthorize runs after the method and can inspect the return value via returnObject. Use it when the authorization decision depends on what was loaded.

@PostAuthorize("returnObject.ownerUsername == authentication.name")
public Document getDocument(Long id) {
    return documentRepository.findById(id).orElseThrow();
}

Warning: @PostAuthorize executes the method first, then denies. Don’t use it for operations with side effects (writes, external calls) — the work already happened before access is denied. Prefer @PreAuthorize for those.

@Secured

@Secured is the older, simpler annotation. It takes literal authority strings (with the ROLE_ prefix) and supports no SpEL.

import org.springframework.security.access.annotation.Secured;

@Secured("ROLE_ADMIN")
public void purge() { ... }

@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void approve(Long id) { ... }
AnnotationWhen it runsSpEL?Argument/return access
@PreAuthorizeBefore methodYes#arg, authentication
@PostAuthorizeAfter methodYesreturnObject, #arg
@SecuredBefore methodNonone

Prefer @PreAuthorize for almost everything — it is more expressive and is the documented modern default.

SpEL building blocks

Inside these expressions you have access to a rich set of security functions and variables:

hasRole('ADMIN')                       // ROLE_ADMIN (prefix added)
hasAnyRole('USER', 'ADMIN')
hasAuthority('orders:read')            // verbatim, no prefix
isAuthenticated()
authentication                         // the Authentication object
authentication.name                    // current username
authentication.principal               // the UserDetails
principal.id                           // your custom field
#id, #form.ownerId                     // method arguments by name
@beanName.check(#id)                   // call a Spring bean method

A common pattern delegates a complex decision to a dedicated bean for readability and reuse:

@PreAuthorize("@accountSecurity.canEdit(#id, authentication)")
public void update(Long id, AccountForm form) { ... }
@Component("accountSecurity")
public class AccountSecurity {
    public boolean canEdit(Long id, Authentication auth) {
        // arbitrary logic, e.g. check ownership in the DB
        return true;
    }
}

What a denial looks like

A failed check throws AccessDeniedException, which Spring maps to 403 for web requests:

HTTP/1.1 403 Forbidden
{"status":403,"error":"Forbidden"}

Pitfalls

  • Self-invocation — calling an annotated method from another method in the same bean bypasses the proxy, so the check does not run. Call through the bean reference or split the methods across beans.
  • @PostAuthorize side effects — the method already executed before denial; avoid for writes.
  • Forgetting @EnableMethodSecurity — annotations are silently ignored without it.
  • securedEnabled is off by default — enable it if you use @Secured.
Last updated June 13, 2026
Was this helpful?