Clean • Professional
Method-Level Security allows you to enforce authorization rules directly on individual methods (service or controller layer), instead of relying only on URL-based security.
👉 It answers the critical question:
“Who is allowed to execute this method?”
Even if an API endpoint is accessible, method-level security guarantees that only authorized users can execute the underlying business logic.

URL-based security alone is often not sufficient.
Problems Without Method-Level Security
Benefits
Spring Boot 3 / Spring Security 6+
@Configuration
@EnableMethodSecurity
publicclassSecurityConfig {
}
This enables:
@PreAuthorize@PostAuthorize@Secured@RolesAllowedSpring Boot 2.x (Legacy)
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
@PreAuthorize@PreAuthorize("hasRole('ADMIN')")
publicvoiddeleteUser(Long id) {
}
Why @PreAuthorize is Preferred
@PostAuthorize@PostAuthorize("returnObject.owner == authentication.name")
public DocumentgetDocument(Long id) {
return documentService.findById(id);
}
Users can access only their own data, even if they guess another ID.
@Secured@Secured("ROLE_ADMIN")
publicvoidcreateUser() {
}
Limitations:
ROLE_ prefix@PreAuthorize@RolesAllowed (JSR-250)@RolesAllowed("ADMIN")
publicvoidupdateUser() {
}
Requires:
@EnableMethodSecurity(jsr250Enabled = true)
Mostly used for standards-based or legacy systems.
@PreAuthorize with SpEL (Spring Expression Language)Spring Security supports SpEL (Spring Expression Language) in the @PreAuthorize annotation to define fine-grained authorization rules at the method level.
@PreAuthorize expressions are evaluated before method execution using data from the SecurityContext, method parameters, and custom beans.
Common Security Expressions
| Expression | Meaning |
|---|---|
hasRole('ADMIN') | User must have ROLE_ADMIN |
hasAnyRole('ADMIN','USER') | User has any one of the roles |
hasAuthority('SCOPE_read') | User has OAuth2 scope read |
hasAnyAuthority('READ','WRITE') | User has any authority |
isAuthenticated() | User is logged in (not anonymous) |
isAnonymous() | User is not authenticated |
authentication.name | Logged-in username |
principal | Current authenticated user object |
principal.claims['email'] | Read JWT / OIDC claim |
#id == authentication.principal.id | Method parameter match |
@bean.method() | Invoke custom authorization logic |
@PreAuthorize("#userId == authentication.principal.id")
public UsergetUserProfile(Long userId) {
return userService.findById(userId);
}
What this does
userId with the logged-in user’s IDEven if the API endpoint is accessible, the method will not execute unless the condition is satisfied.
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public UsergetUser(Long userId) {
return userService.findById(userId);
}
Very common in banking, SaaS, admin dashboards
When using JWT-based authentication:
SecurityContextHolder.getContext().getAuthentication()
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/reports")
public List<Report>getReports() {
return reportService.findAll();
}
JWT
↓
Authorities extracted
↓
SecurityContext
↓
@PreAuthorizecheck
↓
Method execution
In OAuth2 / OIDC:
SCOPE_Scope-Based Access Control
@PreAuthorize("hasAuthority('SCOPE_admin')")
@GetMapping("/api/admin")
public StringadminApi() {
return"Admin API";
}
Common OAuth2 Authorities
SCOPE_readSCOPE_writeSCOPE_adminUsed heavily in API security & resource servers
For complex business rules, use a custom authorization bean.
Method Security
@PreAuthorize("@orderSecurity.isOwner(#orderId)")
public OrderviewOrder(Long orderId) {
return orderService.find(orderId);
}
Custom Security Bean
@Component
publicclassOrderSecurity {
publicbooleanisOwner(Long orderId) {
// custom ownership or tenant logic
returntrue;
}
}
Used for domain-object security (ACLs, multi-tenant apps).
@PreAuthorize("hasPermission(#doc, 'WRITE')")
publicvoideditDocument(Document doc) {
}
Allows higher roles to inherit permissions of lower roles.
Requirement
ADMIN should have all USER permissions
Configuration
@Bean
RoleHierarchyroleHierarchy() {
RoleHierarchyImplhierarchy=newRoleHierarchyImpl();
hierarchy.setHierarchy("""
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
""");
return hierarchy;
}
Result
| Role | Effective Permissions |
|---|---|
| ROLE_ADMIN | MANAGER + USER |
| ROLE_MANAGER | USER |
| ROLE_USER | — |
@PreAuthorize rulesMethodCall
↓
Spring AOP Proxy
↓
Security Expression Evaluation
↓
Access Granted / Denied
↓
Method Executes
| Feature | URL Security | Method Security |
|---|---|---|
| Scope | Protects HTTP endpoints | Protects business logic / methods |
| Granularity | Coarse | Fine-grained |
| Annotations / Config | HttpSecurity rules in SecurityFilterChain | @PreAuthorize, @PostAuthorize, @Secured, @RolesAllowed |
| Expression Support | Limited | Full SpEL support (roles, scopes, claims, params) |
| Internal Calls | Bypassed if called internally | Enforced even for internal method calls |
| Best Use | Simple access rules, public APIs | Complex rules, multi-role access, microservices |
| Fail-Fast | Only at HTTP request | Blocks unauthorized method execution before business logic |
@EnableMethodSecurity@Secured for complex logicROLE_ and non-ROLE authorities@PreAuthorizeMethod-Level Security protects business logic, not just endpoints, ensuring only authorized users can execute methods.
ROLE_TELLER or ROLE_ADMIN control access.ROLE_SUPER_ADMIN can modify sensitive settings.@PreAuthorize("#tenantId == authentication.principal.tenantId")).@PreAuthorize is the most powerful annotationMethod-Level Security is a crucial layer of protection in Spring applications. By securing methods directly, it ensures that only authorized users can execute business logic, even if endpoints are publicly accessible or internally invoked. Combined with JWT, OAuth2, or OIDC, it enables fine-grained, robust, and maintainable access control across microservices, SaaS platforms, banking systems, and enterprise applications.
In short: URL security protects access points, method-level security protects the logic itself.