Spring Method Security: Why Your Controller Needs a Bodyguard;Secure Your Logic in 2026
You secured your URL endpoints. You added a JWT filter. You think you’re safe.
But what happens if a developer on your team accidentally injects your UserService into a public-facing controller? Or if a background job triggers a sensitive method without an HTTP request?
Your URL security is useless there. It’s like having a bouncer at the club entrance, but leaving the VIP room wide open to anyone who snuck in through the kitchen.
I’ve seen production leaks happen exactly like this. The fix isn’t more firewalls. It’s Method Security. Here is how to lock down your actual Java code, regardless of how it’s called.
How to Enable Method Security (The New Way)
If you’re still using @EnableGlobalMethodSecurity, you’re living in the past. Spring Security 6 deprecated it.
The new annotation is cleaner, faster, and enabled by default in some configurations—but don’t rely on magic. Be explicit.
// SecurityConfig.java
// Spring Boot 3.2.1, Spring Security 6.2.0
package app.codegigs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity // This is the switch
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Standard HTTP config
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
}
Note: By default, this enables @PreAuthorize, @PostAuthorize, @PreFilter, and @PostFilter. You don’t need to set prePostEnabled = true anymore—it’s the default.
The Big 4 Annotations
1. @PreAuthorize (The Workhorse)
This is what you’ll use 95% of the time. It runs before the method starts. If the check fails, the method never executes, and your database stays safe.
// DocumentService.java
@Service
public class DocumentService {
// 1. Simple Role Check
@PreAuthorize("hasRole('ADMIN')")
public void deleteEverything() {
// ...
}
// 2. Argument-based Check (SpEL)
// '#username' refers to the method argument
@PreAuthorize("#username == authentication.name")
public UserProfile getProfile(String username) {
return userRepo.findByUsername(username);
}
}
Line 13 is the killer feature. You can’t do that with URL security. It ensures that user “Dave” can only request “Dave’s” profile. Clean. Simple.
2. @PostAuthorize (The “Check it Later” approach)
Sometimes you don’t know if a user is allowed to see something until you fetch it from the database. That’s where @PostAuthorize comes in.
Warning: The method actually runs. The database query actually happens. Spring just blocks the return value at the last second.
// OrderService.java
@Service
public class OrderService {
// 'returnObject' is a special variable in SpEL
@PostAuthorize("returnObject.owner == authentication.name")
public Order getOrder(Long id) {
// This query RUNS.
// Be careful with expensive operations here.
return orderRepo.findById(id);
}
}
This blew up a performance review I did last year. A team was fetching a massive PDF blob, then discarding it because of a @PostAuthorize check. We fixed it by moving the check to the SQL query itself.
3. @PreFilter (The Input Cleaner)
This one is magic. It takes a Collection passed into your method and silently removes items the user isn’t allowed to touch.
// ContactService.java
// 'filterObject' refers to the current item in the list
@PreFilter("filterObject.owner == authentication.name")
public void updateContacts(List<Contact> contacts) {
// If you pass 10 contacts, and I only own 3...
// ...this list will only have 3 items when it gets here.
repo.saveAll(contacts);
}
This is safer than relying on the frontend to filter the list. Never trust the frontend.
4. @PostFilter (The Output Sanitizer)
Same as above, but for the return value. It filters the list after you fetch it.
// ProjectService.java
@PostFilter("filterObject.isPublic() or filterObject.owner == authentication.name")
public List<Project> getAllProjects() {
// Fetches EVERYTHING
return projectRepo.findAll();
}
Performance Warning: If findAll() returns 10,000 rows, Spring has to loop through all 10,000 in memory to filter them. If you have a large dataset, do the filtering in the database query instead.
Common Pitfalls
The Self-Invocation Trap
Spring Security uses AOP proxies. This means the security magic only happens when the method is called from outside the class.
public class MyService {
// Calling this from outside? Secure.
@PreAuthorize("hasRole('ADMIN')")
public void secureMethod() { ... }
public void publicMethod() {
// Calling it from inside? SECURITY BYPASSED.
// The proxy doesn't intercept 'this.method()' calls.
this.secureMethod();
}
}
I see this bug in junior developer code reviews constantly. If you need to call a secured method from within the same class, you need to self-inject the service or restructure your code.
Master Method Security
Ready to build complex permission systems using custom SpEL expressions and ACLs? Join 50,000+ developers in the Spring Security Masterclass.
Conclusion
Method Security is your second line of defense. It’s granular, it lives with your business logic, and it protects you when the web layer fails.
Start by adding @EnableMethodSecurity today. Then, find your most sensitive service method and slap a @PreAuthorize on it.
If you’re struggling with how to write complex rules inside these annotations (like “Owner OR Admin OR Manager”), check out our guide on Custom Security Expressions next.