Learnitweb

Method level secturity at resource server

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 @EnableGlobalMethodSecurity. 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 session
  • denyAll – The method is not allowed under any circumstances; note that in this case, the Authentication is never retrieved from the session
  • hasAuthority – The method requires that the Authentication have a GrantedAuthority that matches the given value
  • hasRole – A shortcut for hasAuthority that prefixes ROLE_ or whatever is configured as the default prefix
  • hasAnyAuthority – The method requires that the Authentication have a GrantedAuthority that matches any of the given values
  • hasAnyRole – A shortcut for hasAnyAuthority that prefixes ROLE_ or whatever is configured as the default prefix
  • hasPermission – A hook into your PermissionEvaluator instance for doing object-level authorization

And here is a brief look at the most common fields:

  • authentication – The Authentication instance associated with this method invocation
  • principal – The Authentication#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.