How do you implement method-level security with @PreAuthorize/@PostAuthorize?

8 minintermediatepreauthorizepostauthorizemethod-security

Quick Answer

Method-level security lets you declare authorization rules directly on individual service/repository methods (not just URL patterns), enabled via @EnableMethodSecurity. @PreAuthorize evaluates a SpEL expression against the current authenticated principal before the method executes, blocking the call entirely (throwing AccessDeniedException) if it evaluates false; @PostAuthorize evaluates after the method runs, with access to its return value, useful when the authorization decision depends on the result itself (e.g., only the resource's actual owner may see it).

Detailed Answer

URL-pattern-based authorization (.requestMatchers("/admin/**").hasRole("ADMIN")) works well for broad, path-based rules, but many real authorization decisions are more naturally expressed on the method itself — especially in a service layer that might be called from multiple different controllers, or where the decision depends on the specific arguments/data involved.

Enable it on a configuration class:

@Configuration
@EnableMethodSecurity // (replaces the older @EnableGlobalMethodSecurity)
class MethodSecurityConfig { }

@PreAuthorize evaluates a SpEL expression before the method runs — if it evaluates to false, the method body never executes at all, and Spring throws AccessDeniedException:

@PreAuthorize("hasRole('ADMIN')")
void deleteUser(Long userId) { ... }

@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
User getUserProfile(Long userId) { ... } // users can view their own profile, admins can view anyone's

@PostAuthorize evaluates after the method has already run, with access to the method's return value via the returnObject SpEL variable — useful when the authorization decision genuinely depends on the fetched data itself, not just the input arguments:

@PostAuthorize("returnObject.owner == authentication.name")
Document getDocument(Long id) {
    return documentRepository.findById(id).orElseThrow();
    // if the returned Document doesn't belong to the caller, access is denied *after* the fetch
}

Practical note on @PostAuthorize: because the method body already ran (including, potentially, a database read) before the check happens, it's less efficient than a @PreAuthorize check that could reject the call upfront — but it's the only option when the authorization rule can't be evaluated without first knowing what the method would actually return.

Both annotations rely on the same AOP proxy mechanism as @Transactional — meaning the same self-invocation caveat applies: calling a @PreAuthorize-annotated method on this from within the same class bypasses the proxy, and the security check silently never runs.