Skip to content

5.4 Applying method-level security

Although it’s easy to think about security at the web-request level, that’s not always where security constraints are best applied. Sometimes it’s better to verify that the user is authenticated and has been granted adequate authority at the point where the secured action will be performed.

For example, let’s say that for administrative purposes, we have a service class that includes a method for clearing out all orders from the database. Using an injected OrderRepository, that method might look something like this:

java
public void deleteAllOrders() {
  orderRepository.deleteAll();
}

Now, suppose we have a controller that calls the deleteAllOrders() method as the result of a POST request, as shown here:

java
@Controller
@RequestMapping("/admin")
public class AdminController {

private OrderAdminService adminService;

  public AdminController(OrderAdminService adminService) {
    this.adminService = adminService;
  }

  @PostMapping("/deleteOrders")
  public String deleteAllOrders() {
    adminService.deleteAllOrders();
    return "redirect:/admin";
  }

}

It’d be easy enough to tweak SecurityConfig as follows to ensure that only authorized users are allowed to perform that POST request:

java
.authorizeRequests()
  ...
  .antMatchers(HttpMethod.POST, "/admin/**")
        .access("hasRole('ADMIN')")
  ....

That’s great and would prevent any unauthorized user from making a POST request to /admin/deleteOrders that would result in all orders disappearing from the database.

But suppose that some other controller method also calls deleteAllOrders(). You’d need to add more matchers to secure the requests for the other controllers that will need to be secured.

Instead, we can apply security directly on the deleteAllOrders() method like this:

java
`@PreAuthorize` ("hasRole('ADMIN')")
public void deleteAllOrders() {
  orderRepository.deleteAll();
}

The @PreAuthorize annotation takes a SpEL expression, and, if the expression evaluates to false, the method will not be invoked. On the other hand, if the expression evaluates to true, then the method will be allowed. In this case, @PreAuthorize is checking that the user has the ROLE_ADMIN privilege. If so, then the method will be called and all orders will be deleted. Otherwise, it will be stopped in its tracks.

In the event that @PreAuthorize blocks the call, then Spring Security’s AccessDeniedException will be thrown. This is an unchecked exception, so you don’t need to catch it, unless you want to apply some custom behavior around the exception handling. If left uncaught, it will bubble up and eventually be caught by Spring Security’s filters and handled accordingly, either with an HTTP 403 page or perhaps by redirecting to the login page if the user is unauthenticated.

For @PreAuthorize to work, you’ll need to enable global method security. For that, you’ll need to annotate the security configuration class with @EnableGlobalMethodSecurity as follows:

java
@Configuration
@EnableGlobalMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  ...
}

You’ll find @PreAuthorize to be a useful annotation for most method-level security needs. But know that it has a slightly less useful after-invocation counterpart in @PostAuthorize. The @PostAuthorize annotation works almost the same as the @PreAuthorize annotation, except that its expression won’t be evaluated until after the target method is invoked and returns. This allows the expression to consider the return value of the method in deciding whether to permit the method invocation.

For example, suppose we have a method that fetches an order by its ID. If you want to restrict it from being used except by admins or by the user who the order belongs to, you can use @PostAuthorize like this:

java
@PostAuthorize("hasRole('ADMIN') || " +
    "returnObject.user.username == authentication.name")
public TacoOrder getOrder(long id) {
...
}

In this case, the returnObject in the TacoOrder returned from the method. If its user property has a username that is equal to the authentication’s name property, then it will be allowed. To know that, though, the method will need to be executed so that it can return the TacoOrder object for consideration.

But wait! How can you secure a method from being invoked if the condition for applying security relies on the return value from the method invocation? That chickenand-egg riddle is solved by allowing the method to be invoked, then throwing an AccessDeniedException if the expression returns false.

Released under the MIT License.