Method-Level Security
Method-Level Security
The SecurityFilterChain you configured in earlier lessons guards URL paths — it is your perimeter fence. But what happens when two different HTTP endpoints both call the same service method, or when an internal scheduled job invokes business logic that should still enforce access rules? URL-level authorization cannot answer that question. Method-level security moves the enforcement boundary into your service layer, where it belongs for anything beyond the simplest applications.
Spring Security provides two main annotations for this: @PreAuthorize and @Secured. This lesson covers both in depth — what they do, how they differ, when to prefer one over the other, and the security implications of each choice.
Enabling Method-Level Security
Method security is opt-in. Add @EnableMethodSecurity to any configuration class (your main application class or a dedicated security config class). In Spring Security 6 this annotation replaces the older @EnableGlobalMethodSecurity.
new directly), the annotation is silently ignored. Opt-in keeps the behavior explicit and testable.
@PreAuthorize — Expression-Based, Fine-Grained Control
@PreAuthorize evaluates a Spring Expression Language (SpEL) expression before the method body runs. If the expression returns false, Spring throws AccessDeniedException and the method is never entered. This is the annotation you should reach for in nearly every new project.
The #article syntax in the third example is SpEL's way of referencing a method parameter by name. authentication.principal gives you the currently authenticated user object (your custom UserDetails implementation). This means your authorization logic can inspect real domain data — not just role strings — which is what makes @PreAuthorize so powerful.
Common SpEL Expressions
hasRole('ADMIN')— checks forROLE_ADMINin the user's granted authorities (Spring prependsROLE_automatically).hasAnyRole('EDITOR','VIEWER')— true if the user has any of the listed roles.hasAuthority('ARTICLE_DELETE')— checks the exact authority string; useful for fine-grained permission systems that go beyond role names.isAuthenticated()— true for any logged-in user (not anonymous).isAnonymous()— true only for unauthenticated requests.authentication.name == 'system'— compare the username directly.#param— reference a method parameter namedparam.@myBean.check(#param)— delegate to a Spring bean method for complex logic.
@Component SecurityService — and reference it in SpEL: @PreAuthorize("@securityService.isProjectMember(#projectId, authentication)"). This keeps your annotations readable and your logic unit-testable.
@PostAuthorize — Authorize on the Return Value
Sometimes you do not know whether access should be granted until after the method has loaded data from the database. @PostAuthorize runs its SpEL expression after the method returns, with the return value available as returnObject.
@PostAuthorize on a method that has side effects (writes, emails, payments) — use @PreAuthorize with a pre-loaded permission check instead.
@Secured — Simple Role Checking
@Secured is an older annotation that accepts a list of authority strings. The method executes only if the authenticated user holds at least one of them. Unlike @PreAuthorize, it does not support SpEL — no parameter references, no bean delegation, no compound expressions.
Notice the full ROLE_ prefix is required with @Secured, whereas hasRole() in SpEL adds it automatically.
To activate @Secured you must enable it explicitly:
@PreAuthorize vs @Secured — When to Use Which
- Use
@PreAuthorizefor all new code. It is more expressive, supports SpEL, works with both roles and fine-grained authorities, and can inspect method arguments. - Use
@Securedonly when you need to support a legacy codebase that already uses it, or when you want a visual signal that a method performs only a simple role check and nothing more complex.
Applying Annotations at the Class Level
You can place @PreAuthorize on a class to set a default policy for all its methods, then override it per-method where needed:
Testing Method Security
Spring Security Test provides @WithMockUser and @WithUserDetails to simulate authenticated contexts in unit tests. This lets you verify authorization rules without a running server.
@PreAuthorize annotation is a silent security hole. Always write tests that confirm unauthorized users are rejected, not just that authorized users are permitted.
Practical Pitfalls
- Self-invocation bypasses the proxy. If a method in the same class calls another annotated method directly (not through the Spring proxy), the annotation is not evaluated. Extract the method to a separate bean if you need it enforced on internal calls.
- Interface vs. implementation. Annotate the implementation class methods, not the interface methods, unless you are using interface-based proxies (unusual in Spring Boot). Spring Security 6 supports both, but placing annotations on the implementation is safer and more visible.
- Don't replace URL security. Method security complements URL authorization — it does not replace it. Keep both layers. URL rules reject bad requests at the perimeter; method rules enforce business invariants deep in the stack.
Summary
Method-level security lets you attach authorization rules directly to your service methods using @PreAuthorize (SpEL, expressive, preferred) or @Secured (simple role strings, legacy-compatible). Enable the feature with @EnableMethodSecurity on a configuration class. Use SpEL parameter references and bean delegation to keep complex rules testable. Always pair method security with URL authorization — the two layers together give you defense in depth. In the next lesson you will put everything together by securing a complete web application end-to-end.