1. Introduction
In this tutorial, we’ll use method level security to protect our web service endpoint. Using method-level security, we can apply annotations above the method name to either permit or block the execution of the method depending on specific conditions. For instance, we can apply a security annotation to restrict the method’s execution to users with a particular role or authority. Security annotations can be applied not only in the controller class, where the request mapping methods are defined, but also in the service layer class if necessary.
In addition to modeling authorization at the request level, Spring Security also supports modeling at the method level.
You can activate it in your application by annotating any @Configuration
class with @EnableMethodSecurity
. Then, you are immediately able to annotate any Spring-managed class or method with @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
to authorize method invocations, including the input parameters and return values.
Spring Boot Starter Security does not activate method-level authorization by default.
Spring Security’s method-level authorization is useful for:
- Implementing fine-grained authorization logic, such as when method parameters and return values influence the authorization decision.
- Applying security at the service layer.
- Preferring annotation-based configurations over HttpSecurity-based setups.
@EnableMethodSecurity
supercede @EnableGlobalMethodSecurit
y. This annotation favors direct bean-based configuration and is built using native Spring AOP. This annotation enables @PreAuthorize
, @PostAuthorize
, @PreFilter
, and @PostFilter
by default.
It is not supported to repeat the same annotation on the same method. For example, you cannot place @PreAuthorize
twice on the same method. Instead, use SpEL’s boolean support or its support for delegating to a separate bean.
It’s important to remember that when you use annotation-based Method Security, then unannotated methods are not secured.
2. @secured
@Secured
is a legacy option for authorizing invocations. @PreAuthorize
supercedes it and is recommended instead.
To use the @Secured
annotation, you should first change your Method Security declaration to enable it like so:
@EnableMethodSecurity(securedEnabled = true)
Following are examples of using @Secured
:
@Secured ({"ROLE_USER"}) public void create(Contact contact); @Secured ({"ROLE_USER", "ROLE_ADMIN"}) public void update(Contact contact);
3. @PreAuthorize
@Component public class ContactService { @PreAuthorize("hasRole('ADMIN')") public Contact deleteContact(Long id) { // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority } }
This indicates that the method can only be executed if the provided expression hasRole('ADMIN')
evaluates to true. @PreAuthorize
also can be a meta-annotation, be defined at the class or interface level, and use SpEL Authorization Expressions.
4. @PostAuthorize
When Method Security is enabled, you can use the @PostAuthorize
annotation on a method as follows:
@Component public class ContactService { @PostAuthorize("returnObject.owner == authentication.name") public Account readContact(Long id) { // ... is only returned if the `Contact` belongs to the logged in user } }
This is meant to indicate that the method can only return the value if the provided expression returnObject.owner == authentication.name
passes. returnObject
represents the Contact
object to be returned.
5. @PreFilter
When Method Security is active, you can annotate a method with the @PreFilter
annotation like so:
@Component public class ContactService { @PreFilter("filterObject.owner == authentication.name") public Collection<Contact> updateContactss(Contact... contacts) { // ... `contacts` will only contain the contacts owned by the logged-in user return updated; } }
This is meant to filter out any values from contacts
where the expression filterObject.owner == authentication.name
fails. filterObject
represents each contact in contactss and is used to test each contact. @PreFilter
supports arrays, collections, maps, and streams (so long as the stream is still open).
For example, the above updateContacts
declaration will function the same way as the following:
@PreFilter("filterObject.owner == authentication.name") public Collection<Contact> updateContacts(Contact[] contacts) @PreFilter("filterObject.owner == authentication.name") public Collection<Contact> updateContacts(Collection<Contact> contacts)
The result is that the above method will only have the Contact
instances where their owner attribute matches the logged-in user’s name.
6. @PostFilter
When Method Security is active, you can annotate a method with the @PostFilter
annotation like so:
@Component public class ContactService { @PostFilter("filterObject.owner == authentication.name") public Collection<Contact> readContact(String... ids) { // ... the return value will be filtered to only contain the contacts owned by the logged-in user return contacts; } }
This is meant to filter out any values from the return value where the expression filterObject.owner == authentication.name
fails. filterObject
represents each contact in contacts and is used to test each contact.
@PostFilter
supports arrays, collections, maps, and streams (so long as the stream is still open).
For example, the above readContacts
declaration will function the same way as the following:
@PostFilter("filterObject.owner == authentication.name") public Contact[] readContacts(String... ids) @PostFilter("filterObject.value.owner == authentication.name") public Map<String, Contact> readContactss(String... ids) @PostFilter("filterObject.owner == authentication.name") public Stream<Contact> readContactss(String... ids)
The result is that the above method will return the Contact
instances where their owner attribute matches the logged-in user’s name.
7. Declaring Annotations at the Class or Interface Level
It’s also supported to have Method Security annotations at the class and interface level.
@Controller @PreAuthorize("hasAuthority('ROLE_MEMBER')") public class CustomController { @GetMapping("/customEndpoint") public String customEndpoint() { ... } }
All methods inherit the class-level behavior. Methods declaring the annotation override the class-level annotation. The same is true for interfaces, with the exception that if a class inherits the annotation from two different interfaces, then startup will fail. This is because Spring Security has no way to tell which one you want to use.
8. Using Meta Annotations
Method Security supports meta annotations. This means that you can take any annotation and improve readability based on your application-specific use cases.
For example, you can simplify @PreAuthorize("hasRole('ADMIN')"
) to @IsAdmin
like so:
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") public @interface IsAdmin {}
And the result is that on your secured methods you can now do the following instead:
@Component public class BankService { @IsAdmin public Account readAccount(Long id) { // ... is only returned if the `Account` belongs to the logged in user } }
This results in more readable method definitions.
9. Using Authorization Expression Fields and Methods
The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. What follows is a quick overview of the most common methods:
permitAll
– The method requires no authorization to be invoked; note that in this case, the Authentication is never retrieved from the sessiondenyAll
– The method is not allowed under any circumstances; note that in this case, the Authentication is never retrieved from the sessionhasAuthority
– The method requires that the Authentication have aGrantedAuthority
that matches the given valuehasRole
– A shortcut forhasAuthority
that prefixesROLE_
or whatever is configured as the default prefixhasAnyAuthority
– The method requires that the Authentication have aGrantedAuthority
that matches any of the given valueshasAnyRole
– A shortcut forhasAnyAuthority
that prefixesROLE_
or whatever is configured as the default prefixhasPermission
– A hook into yourPermissionEvaluator
instance for doing object-level authorization
And here is a brief look at the most common fields:
authentication
– TheAuthentication
instance associated with this method invocationprincipal
– TheAuthentication#getPrincipal
associated with this method invocation
10. Conclusion
In this tutorial, we explored how to implement method-level security in a resource server, a critical component for ensuring that only authorized users can access specific parts of your application. By applying annotations such as @PreAuthorize
and @Secured
, we can enforce role-based access control and fine-grained permissions at the method level, enhancing the overall security of the system.
This approach ensures that even if unauthorized users gain access to the application, they are restricted from executing sensitive actions or accessing protected resources. Integrating method-level security not only strengthens your application but also makes it more scalable and adaptable to evolving security needs.
By following these best practices, you can ensure a more secure environment for your application, safeguarding your resources effectively.