Spring Security (series) - Method authorization #7

Spring Security can go deeper than just URL matchers! 🐳

Brief

When adding authorization rules to our application, we can either:

  • do the checking at the URL level, like we did before with the MVC matchers.
  • add them directly on the service methods. This way, we are able to implement Spring Security even though we don't have a web application.

In this post, we'll implement the second approach with the help of some Spring annotations: @PreAuthorize, @PostAuthorize, @PreFilter and @PostFilter.

Implementation

🧠  Before we start, you need to keep in mind that method-level security is implemented using Spring AOP.

This means that if a secured method is called by another method of the same class, the security rules will be ignored.

All these annotations support SpEL (Spring Expression Language) written predicates. Predicates can be applied on SecurityContext information along with input parameters and output results.

Example:

@PreAuthorize("#user == authentication.principal.username")
public Integer getScore(String user) { //...}

Now, let's take a look at these 4 annotations one by one and see what they can do for us. 👀

  • @PreAuthorize - Apply the predicate and if it fails, the method won't be called and a 403 Forbidden response will be returned.
  • @PostAuthorize - Apply the rule after the method is called and if it fails, the response won't be returned.
    • This annotation is used to apply rules on the returned object. Don't use it with "mutating" actions because they will still be executed.
  • @PreFilter - Filter the parameter list, based on the predicate, before the actual method call.
  • @PostFilter - Applies the rule to each item of the output and returns only the items which conform to the predicate.

Let's see them in action! 🚀

First, we need some configuration in place.

🔵  On the SecurityConfig class we need to add the following annotation:

@EnableGlobalMethodSecurity(prePostEnabled = true) – so that the annotations work ( @PreAuthorize , etc..)

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
---
SecurityConfig.java

❗️ If you code with me and work based on the previous implementation, you will need to remove this method, so that you don't use the MVC matchers too.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic();
        http.authorizeRequests().mvcMatchers("/admin").hasRole("ADMIN");
        http.authorizeRequests().mvcMatchers("/client").hasRole("CLIENT");
    }
SecurityConfig.java

🔵  We need a service with some methods that we will secure.

@Service
@RequiredArgsConstructor
public class DemoService {

    @PreAuthorize("hasRole('ADMIN')")
    public String preAuthorize() {
        return "Test";
    }

    @PostAuthorize("returnObject == authentication.name")
    public String postAuthorize() {
        return "andrei@mail.com";
    }

    @PreFilter("filterObject == authentication.name")
    public List<String> preFilter(List<String> userNames) {
        return userNames;
    }

    @PostFilter("filterObject == authentication.name")
    public List<String> postFilter() {
        return List.of("andrei@mail.com", "mark@mail.com");
    }
}
DemoService.java

🔵  Add a Controller just to test it easier, exposing the secured methods on some endpoints. We don't really need it, we could test it using some integration tests as well.

@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class Controller {
    private final DemoService demoService;


    @GetMapping("/preAuthorize")
    public String preAuthorize() {
        return demoService.preAuthorize();
    }


    @GetMapping("/postAuthorize")
    public String postAuthorize() {
        return demoService.postAuthorize();
    }


    @GetMapping("/preFilter")
    public List<String> preFilter(@RequestParam List<String> userNames) {
        return demoService.preFilter(userNames);
    }


    @GetMapping("/postFilter")
    public List<String> postFilter() {
        return demoService.postFilter();
    }
}
Controller.java

🐞 Test it with Postman:

  • PreAuthorize
  • PostAuthorize
  • PreFilter
  • PostFilter

As always, you can find the full implementation in the Github repo! 🚀


💡
Don't miss out on more posts like this! Susbcribe to our free newsletter!