10 Global Methods How to Ensure Security Access at the Method Level

10 Global Methods How to Ensure Security Access at the Method Level #

So far, we have systematically introduced the authentication and authorization processes in Spring Security. However, please note that the objects we are discussing are web applications, which means that the authenticated and authorized resources are a series of HTTP endpoints. But what if we are developing something other than a web application? Can authentication and authorization still be effective? The answer is yes. Today, we will discuss method-level security access strategies to ensure the security of every component in a regular application.

Global Method Security Mechanism #

Before we clearly understand the mechanism of method-level security, let’s analyze the various layers/components a typical application consists of. Taking a Spring Boot application as an example, we can use the classic layered architecture, which divides the application into the web layer, service layer, and repository layer. Please note that the service layer in the three-layer architecture may call other third-party components.

In each layer/component, there are implementation methods provided for specific business logic. We can apply security control to these methods. Therefore, you can think of this security control as not only targeting the web layer component but also globally at the method level. Thus, it is called the Global Method Security mechanism.

So, what value can the Global Method Security mechanism bring to us? Generally, it includes two aspects: method invocation authorization and method invocation filtering.

The meaning of method invocation authorization is clear. Just like the authorization mechanism at the endpoint level, we can use it to determine if a request has the permission to invoke a method. If the authorization management is done before the method invocation, it is called pre-authorization; if it is done after the execution of the method to determine whether the caller can access the returned result, it is generally called post-authorization.

Method invocation filtering is essentially similar to a filtering mechanism and can be divided into two categories: PreFilter and PostFilter. The PreFilter is used to filter the parameters of the method, thus obtaining the content received by the parameters. The PostFilter is used to determine the content that the caller can receive from the method’s returned result after the method execution.

Please note that by default, Spring Security does not enable the Global Method Security mechanism. Therefore, to enable this feature, we need to use the @EnableGlobalMethodSecurity annotation. As shown in the example in this column, the general approach is to create a separate configuration class and add this annotation to the configuration class, as shown below:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    ...
}

Please note that when using the @EnableGlobalMethodSecurity annotation, we set “prePostEnabled” to true, which means that we enable Pre/PostAuthorization annotations, which are not effective by default. At the same time, we also need to know that Spring Security provides three implementation methods for implementing the Global Method Security mechanism. In addition to the Pre/PostAuthorization annotations, we can also use the @RolesAllowed annotation and the @Secured annotation based on the JSR 250 specification. In this column, we only discuss the most commonly used Pre/PostAuthorization annotations. Now let’s look at the specific usage.

Implementing Method-Level Authorization Using Annotations #

For method-level authorization, Spring Security provides two annotations: @PreAuthorize and @PostAuthorize, which are used for pre-authorization and post-authorization respectively.

@PreAuthorize Annotation #

Let’s first look at the use case of the @PreAuthorize annotation. Suppose there is a web layer component called OrderController in a Spring Boot web application. This controller calls a component in the service layer called OrderService. We want to add permission control capabilities to requests that access methods in the OrderService layer. That is, only requests with the “DELETE” permission can execute the deleteOrder() method in OrderService, and requests without this permission will throw an exception directly, as shown in the following diagram:

Drawing 0.png

Illustration of Pre-Authorization in the Service Layer Component

Obviously, the process described above targets the use case of pre-authorization. Therefore, we can use the @PreAuthorize annotation for this:

The definition of the @PreAuthorize annotation is as follows:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
    // Set access control by passing a SpEL expression
    String value();
}

As we can see, the @PreAuthorize annotation functions similarly to the access() method introduced in Lecture 05 - “Access Authorization: How to Effectively Configure Secure Access to Requests”, both of which set access control rules by passing in a SpEL expression.

To integrate the @PreAuthorize annotation into our application, we can create a security configuration class as shown below, where we add the @EnableGlobalMethodSecurity annotation:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    ...
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetailsService service = new InMemoryUserDetailsManager();

        UserDetails u1 = User.withUsername("jianxiang1")
                              .password("12345")
                              .authorities("WRITE")
                              .build();

        UserDetails u2 = User.withUsername("jianxiang2")
                              .password("12345")
                              .authorities("DELETE")
                              .build();

        service.createUser(u1);
        service.createUser(u2);

        return service;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

Here, we created two users “jianxiang1” and “jianxiang2” with the “WRITE” and “DELETE” authorities respectively. Then, we implemented the deleteOrder() method in OrderService as shown below:

@Service
public class OrderService {
    @PreAuthorize("hasAuthority('DELETE')")
    public void deleteOrder(String orderId) {
        ...
    }
}

As we can see, here we use the @PreAuthorize annotation to implement pre-authorization. In this annotation, we use the familiar hasAuthority('DELETE') method to determine if the request has the “DELETE” authority.

The case described above is relatively simple. Let’s now look at a more complex scenario that integrates with the user authentication process.

Suppose there is a method called getOrderByUser(String user) in OrderService, and for security reasons, we only want users to be able to get information about the orders they created. In other words, we need to verify if the “user” parameter passed to this method is a valid user authenticated at the moment. In this case, we can use the @PreAuthorize annotation:

@PreAuthorize("#name == authentication.principal.username")

public List<Order> getOrderByUser(String user) {

    …

}

Here, we compare the input parameter “user” with the “authentication.principal.username” obtained from the security context using SpEL expression. If they are the same, the method logic is executed correctly. Otherwise, an exception is thrown directly.

@PostAuthorize Annotation #

Compared to the @PreAuthorize annotation, the application scenarios for the @PostAuthorize annotation may be less common. Sometimes, we want to allow the caller to call the method correctly, but we don’t want the caller to receive the returned response result. This may sound a bit strange, but in applications that access third-party external systems, we cannot fully trust the correctness of the returned data, and there is a need to restrict the response results of the call. The @PostAuthorize annotation provides a good solution for implementing such requirements, as shown below:

Drawing 1.png

Illustrative diagram of post-authorization for service layer components

To demonstrate the @PostAuthorize annotation, let’s first set a specific return value. Suppose we have an Author object as shown below, which contains the name of the author and the books they wrote:

public class Author {

    private String name;

    private List<String> books;

}

Furthermore, let’s assume there are two Author objects saved in the system as shown below:

Map<String, Author> authors =

    Map.of("AuthorA", new Author("AuthorA", List.of("BookA1", "BookA2")),

        "AuthorB", new Author("AuthorB", List.of("BookB1"))

    );

Now, we have a query method to get the Author object based on the name:

@PostAuthorize("returnObject.books.contains('BookA2')")

public Author getAuthorByNames(String name) {

    return authors.get(name);

}

As you can see, by using the @PostAuthorize annotation, we can determine the authorization result based on the return value. In this example, by using the “returnObject” object that represents the return value, if we call this method with the “AuthorA” who wrote “BookA2”, the data will be returned normally. If we use “AuthorB”, a 403 exception will be thrown.

Implementing Method-Level Filtering Using Annotations #

For method-level filtering, Spring Security also provides a pair of annotations, namely @PreFilter and @PostFilter, used for pre-filtering and post-filtering respectively.

@PreFilter Annotation #

Before introducing the use of the @PreFilter annotation for method-level filtering, we need to clarify the difference between it and the @PreAuthorize annotation. Through pre-authorization, if the method call parameters do not meet the permission rules, the method will not be called. However, with pre-filtering, the method call will always be executed, but only the data that meets the filtering rules will be passed to the next level of components in the call chain.

Next, let’s look at the usage of the @PreFilter annotation. We design a new data model and construct a method in the Controller layer as shown below:

@Autowired
private ProductService productService;

@GetMapping("/sell")
public List<Product> sellProduct() {

        List<Product> products = new ArrayList<>();

        products.add(new Product("p1", "jianxiang1"));
        products.add(new Product("p2", "jianxiang2"));
        products.add(new Product("p3", "jianxiang3"));

        return productService.sellProducts(products);
}

In the above code, the Product object contains the product number and username. Then, we move to the Service layer component and implement the following method:

@PreFilter("filterObject.name == authentication.name")
public List<Product> sellProducts(List<Product> products) {

        return products;

}

Here, we use the @PreFilter annotation to filter the input data. By using the “filterObject” object, we can access the input Product data and compare the “filterObject.name” field with “authentication.name” obtained from the security context. This way, we can filter out the data that does not belong to the currently authenticated user.

@PostFilter Annotation #

Similarly, to better understand the meaning of the @PostFilter annotation, we compare it with the @PostAuthorize annotation. Similarly, through post-authorization, if the method call parameters do not meet the permission rules, the method will not be called. If post-filtering is used, the method call will still be executed, but only the data that meets the filtering rules will be returned.

The usage of the @PostFilter annotation is also simple, as shown below:

@PostFilter("filterObject.name == authentication.principal.username")
public List<Product> findProducts() {

        List<Product> products = new ArrayList<>();

        products.add(new Product("p1", "jianxiang1"));
        products.add(new Product("p2", "jianxiang2"));
        products.add(new Product("p3", "jianxiang3"));

        return products;

}

With the @PostFilter annotation, we specify the filter rule as “filterObject.name == authentication.principal.username”, which means that this method will only return data that belongs to the currently authenticated user, and the data of other users will be automatically filtered out.

Through the above examples, you may have realized the subtle relationship between the annotations. For example, the effect of the @PreFilter annotation is actually similar to that of the @PostAuthorize annotation, but the processing directions of the two annotations for data are opposite. In other words, the @PreFilter annotation controls the input of data from the Controller layer to the Service layer, while the @PostAuthorize annotation restricts the data returned from the Service layer to the Controller layer. In the daily development process, you need to pay attention to the direction of data flow in the business scenario in order to choose the appropriate authorization or filtering annotation correctly.

Summary and Preview #

In this lesson, we shifted the focus from securing HTTP endpoints to securing ordinary methods at the method-level. Spring Security provides a set of useful built-in annotations that allow developers to implement global method security mechanisms, including @PreAuthorize and @PostAuthorize annotations for method-level authorization and @PreFilter and @PostFilter annotations for method-level filtering. We have provided descriptions and sample code for using these annotations.

The summary of this lesson is as follows:

Drawing 2.png

Here is a question for you to think about: Regarding the global method security mechanism provided by Spring Security, can you describe the difference between method-level authorization and method-level filtering, as well as their respective application scenarios? Feel free to write down your thoughts in the comments.