14) Spring Security Lesson

Spring Method Security

15 min to complete · By Ryan Desmond, Jared Larsen

The last lesson showed you how to add authorization controls at the request level using your SecurityFilterChain bean. This is a great way to separate your security logic from other application components. However, sometimes your security logic is dependent on a variable outside the scope of the SecurityFilterChain bean.

In these situations, Spring Security also supports authorization at the method level. To activate Spring's method-level security, add the following annotation to any @Configuration class:

@EnableMethodSecurity

Method Security Annotations

As the name suggests, these annotations (we'll call them MSAs for short) restrict access to a method. Depending on the type of MSA, the annotation will instruct Spring to evaluate whether the current user has permission to access — or in other words, execute — the annotated method. Here is a quick summary:

  • @PreAuthorize and @PostAuthorize allow you to evaluate authorization before or after executing the method.
  • @PreFilter and @PostFilter allow you to filter the input or output of the method before or after execution.

These annotations cover a large number of situations, to the point that you may never have to create your own AuthorizationManagers unless you want to. It's time to go over these annotations one by one.

@PreAuthorize and @PostAuthorize

Before an explanation of how these annotations work, it is helpful to highlight the two differences between them:

  1. @PreAuthorize expressions are evaluated before the method executes, while @PostAuthorize expressions are evaluated after.
  2. @PreAuthorize acts only on the method's parameters, while @PostAuthorize also has access to the data returned from the method.

Beyond this, the two annotations are basically the same. They are responsible for allowing or denying access based on expressions. For example, to restrict a method to a specific role you can use the hasRole() expression:

@Service
public class MethodSecurityExample {

    @Autowired
    EntityRepository entityRepo;

    // users with ROLE_USER are allowed to create
    @PreAuthorize("hasRole('USER')")
    public Entity create(Entity entity) {
        return entityRepo.save(entity);
    }
}

This is the same hasRole() method that you utilize in your SecurityFilterChain bean, and as such it automatically adds the ROLE_ prefix to the input. You can use hasAuthority() as well:

// users with the CREATE authority are allowed to create
@PreAuthorize("hasAuthority('CREATE')")
public Entity create(Entity entity) {
    return entityRepo.save(entity);
}
Colorful illustration of a light bulb

Tip: You can use MSAs on any method in your application. This example annotated an @Service method, but you can also annotate your @Controller methods. It's a personal choice based on your requirements.

As you've probably gathered, you can access a bunch of authorization methods from these expressions. Some of the most common methods include:

  • permitAll(): anybody can execute this method
  • denyAll(): nobody can execute this method
  • hasAuthority(): requires a GrantedAuthority whose name matches the input
  • hasAnyAuthority(): requires a GrantedAuthority that matches any of multiple inputs
  • hasRole(): shortcut for hasAuthority() that prefixes ROLE_ to the input
  • hasAnyRole(): shortcut for hasAnyAuthority() that prefixes ROLE_ to the input
  • hasPermission() - taps into your PermissionEvaluator for authorization, you'll learn more about this in the next lesson!

These annotations are extremely powerful and allow you to pass expressions far beyond just verifying roles and authorities. In fact, these are actually SpEL expressions that also have access to the current Authentication. Here's an example that restricts read access to an entity unless they are the owner:

@PostAuthorize(
        "returnObject.ownerUsername == authentication.principal.username")
public Entity read(Long id) {
    return entityRepo.findById(id);
}

The above example ensures that only the entity owner can read it using @PostAuthorize. This means that the method is executed when a request is received, but a Spring Expression Language (SpEL) expression is evaluated before the data is returned.

The SpEL expression is where the actual security logic is implemented. Above, it requires that the ownerUsername field on the Entity returned matches the username of the user who is currently signed in.

By using returnObject, you can interact with whatever is being returned by the method. You can refer to fields directly in these expressions, even if they are private. For example, the above @PostAuthorize used returnObject.ownerUsername to access a private variable.

Colorful illustration of a light bulb

Tip: It is important to remember that only @PostAuthorize can use returnObject, since @PreAuthorize is executed before the return value is known.

Now, let's take a look at a @PreAuthorize example that makes use of a method parameter and the @PostAuthorize that does the same, only after execution:

@PreAuthorize("#id != 1")
public Entity read(Long id) {
    ...
}
@PostAuthorize("#id != 1")
public Entity read(Long id) {
    ...
}

Both examples allow a user to access all entities other than the entity with ID 1. You can see that this is done using #id != 1. This expression references the id parameter of the read() method by using a #. The # lets Spring know you're referencing a parameter name.

While both of these examples use the same SpEL expression, @PreAuthorize is more efficient. It blocks the request before a read database call has to be made, whereas @PostAuthorize waits until after.

Colorful illustration of a light bulb

Tip: It is recommended that you always use @PreAuthorize unless need access to the return object.

@PreFilter and @PostFilter

These annotations do not block the execution of a method. Instead, they are used to regulate the input and output of data. For example, imagine a method returns a list of all entities, but your application has reserved the first 20 IDs for live testing purposes. Therefore, you don't want entities 1 through 20 to be returned to the user. @PostFilter can help.

@PostFilter("filterObject.id <= 20")

This annotation will take the results of the method, and apply a filter to make sure that it doesn't include any test entities. The key here is that the filter object is removed when the expression is evaluated as false.

You might also want to filter a list passed in as a method parameter using @PreFilter. You could use this to make sure that a list used to access data will never contain a specific value or entity. This example shows you how:

@PreFilter(value = "filterObject != shutdown", filterTarget = "commands")
public void executeCommands(List<String> commands, Long timeTillExecution) {
    ...
}

This @PreFilter statement removes all shutdown from the list of commands, making it impossible to give a shutdown command using this method.

To make this a reality, the example used two annotation parameters. The first is value. value takes in the SpEL expression used to filter the list. The second is filterTarget. filterTarget indicates which method parameter the expression should be applied to. But keep in mind that if there is only a single method parameter, you can simplify the annotation like so:

@PreFilter("filterObject != shutdown")
public void executeCommands(List<String> commands) {
    ...
}
Illustration of a lighthouse

Note: @PreFilter and @PostFilter can only be used to filter Collections. Spring will throw an exception if a parameter or return type is not some child of Collection.

@RolesAllowed

The @RolesAllowed annotation is part of JSR-250 and requires adjustment to your method security annotation:

@EnableMethodSecurity(jsr250Enabled = true)

Like @PreAuthorize, you can use @RolesAllowed to specify which role(s) a user requires to execute the method. As this annotation is not created by Spring, you have to explicitly provide the ROLE_ prefix when specifying roles. The @PreAuthorize annotation is much more powerful and is generally recommended in most cases.

// users with ROLE_USER are allowed to create
@RolesAllowed("ROLE_USER")
public Entity create(Entity entity) {
    return entityRepo.save(entity);
}

MSA Limitations

Method security annotations are extremely helpful in creating security rules, but they have one big drawback you need to be aware of. This drawback has to do with the Aspect-Oriented Programming (AOP) nature of MSAs. Don't worry too much about the AOP stuff. That'll all be explained in the upcoming AOP section. For now, just know that just like with @Transactional, MSA rules won't be applied if the method is called from within the same class.

If you ignore this and depend on MSAs for essential security controls, your application is bound to become vulnerable to unauthorized action.

Learn by Doing

Package: springsecurity.authorization.addingauthorization

Time to flex your creative thinking skills!

  • Come up with clever ways to implement each annotation introduced on this page into the example package you built out in the previous lesson.
  • Create additional service classes or entities, remember - this is a learning exercise! It doesn't need to be too serious or useful; just demonstrate your understanding of each annotation.

All right! Be sure to push your work to GitHub when you're done.

Summary: Spring Method Security Annotations

This lesson covered method security annotations in Spring Security. These can control access to methods by checking the state of some data before or after the method executes. Depending on the requirements, you can use:

  • @PreAuthorize and @PostAuthorize to check method parameters or return data and compare it to other data, such as a user's authentication object.
  • @PreFilter and @PostFilter to access and filter Collections that were either passed in or returned from a method.

These four MSAs secure your application in an elegant way, but sometimes in advanced situations they don't quite fit the bill. The next lesson shows you how to use a PermissionEvaluator to expand their use.