Spring security global method security: pre authorization and post authorization

1. Can non Web applications use Spring Security for authorization?

   so many knowledge points about Spring Security mentioned earlier are designed based on Web applications. So isn't it web applications that can't use Spring Security for authentication and authorization? Spring Security is ideal for scenarios where applications are not used through HTTP endpoints. How to configure authorization levels is described here. We will use this method to configure authorization in web and non web applications, which we call global method security.

  for non Web applications, global method security provides the possibility to implement authorization rules even without using endpoints. In Web applications, this approach enables us to flexibly apply authorization rules at different layers of the application, not just at the endpoint level.

2. Enable global method security

   by default, global method security is disabled, so if you want to use this function, you need to start it first. In addition, global method security provides a variety of methods for application authorization. In short, using global method security, the following two main processes can be completed:

  • Call authorization: determines whether someone can call a method according to some implemented permissions (pre authorization), which determines whether someone can access the content returned after the method is executed (post authorization).
  • Filtering: determines what a method can accept through its parameters (pre filtering) and what the caller can receive from the method after filtering.

2.1 understanding call authorization

  one way to configure authorization rules for use with global method security is to invoke authorization. Calling an authorization method refers to applying authorization rules. These authorization rules will determine whether a method can be called, which allows the method to be called, and then determine whether the caller can access the returned value. Usually, it is necessary to determine whether the caller can access the logic according to the provided parameters or the logic execution results. So let's talk about invoking authorization and then apply it to the example.

  how does global method security work? What is the mechanism behind the application of authorization rules? When we enable global method security in our application, we actually enable a Spring aspect processing. This aspect process will intercept the calls to the methods to which the authorization rules apply, and decide whether to forward the calls to the intercepted methods according to these authorization rules.


  
Many implementations in the Spring framework rely on aspect oriented programming (AOP). Global method security is just one of many components in Spring applications that depend on aspects. In short, we classify call authorization as:

  • Pre authorization: the framework checks the authorization rules before method calls.
  • Post authorization: the framework checks authorization rules after method calls.

2.1.1 use pre authorization to protect access to methods

  suppose there is a findDocumentsByUser(String username) method, which will return the document of the specified user for the caller. The caller provides the user name that the method will use to retrieve the document through the parameters of the method. Suppose you need to ensure that authenticated users have intelligent access to their own documents. Can a rule be applied to the method so that the method call is only allowed to execute when the user name of the authenticated user is received as a parameter? The answer is yes! This is what you can do with pre authorization.

   when applying the authorization rule that completely prohibits anyone from calling a method under specific circumstances, we call this processing pre authorization. This approach means that the framework verifies the authorization conditions before executing the method. If the caller does not have permission according to the defined authorization rules, the framework will not delegate the call to the method. The framework throws an exception instead. This is by far the most commonly used global security method.

  usually, if some conditions are not met, you may not want to perform a function at all. In this case, you can apply conditions according to the authenticated user, and you can also reference the value received by the method through its parameters.

2.1.2 authorization protection method call after use

   when applying authorization rules, if you intend to allow someone to call a method, but do not necessarily get the result returned by the method, you can use post authorization. After using authorization, Spring Security will check the authorization rules after the method is executed. You can use this authorization to restrict access to method returns under certain conditions. Because post authorization occurs after the method is executed, authorization rules can be applied to the results returned by the method.

   generally, we apply authorization rules according to the content returned after the method is executed. But be careful to authorize after use! If the method changes during its execution, it will change whether the final authorization is successful or not.

   tip: even if the @ Transactional annotation is used, if the post authorization fails, the change will not be rolled back. The exception thrown by the post authorization function occurs after the transaction manager commits the transaction.

2.2 enable global method security in the project

  a project will be used here to apply the pre authorization and post authorization features provided by global method security. In the Spring Security project, global method security is not enabled by default. To use it, you need to enable it first. However, enabling this feature is simple. You can do this simply by using the EnableGlobalMethodSecurity annotation on the configuration class.

   global method security provides us with the authorization rules defined in the following three methods:

  • Pre authorization / post authorization note
  • JSR 250 annotation, @ RolesAllowed
  • @Secured annotation

   because pre authorization / post authorization annotation is the only method used in almost all cases, we will discuss this method here. To enable this method, you need to use the prePostEnabled property of the EnableGlobalMethodSecurity annotation.

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig {

}

  global method security can be used in any authentication method, from HTTP Basic authentication to OAuth2. To keep things simple and focus on new details, the global method security of HTTP Basic authentication is introduced here. For this reason, the POM The XML file requires only web and Spring Security dependencies.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. Apply pre authorization to permissions and roles

  here is an example of pre authorization. Pre authorization means defining authorization rules for Spring Security applications before calling specific methods. If these rules are violated, the framework will not call the method.

  the application implemented here has a simple scenario. It exposes an endpoint / Hello that returns the string "hello" followed by a name. To get the name, the controller needs to call a service method. This method will apply pre authorization rules to verify whether the user has write permission.

  UserDetailsService and PasswordEncoder are added here to ensure that some users can authenticate. To validate the solution, two users are required: one with write permission and the other without write permission. This proves that the first user can successfully call the endpoint, while for the second user, the application will throw an authorization exception when trying to call the method.

3.1 configuration class of userdetailsservice and Password

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        var service = new InMemoryUserDetailsManager();

        var u1 = User.withUsername("natalie")
                    .password("12345")
                    .authorities("read")
                .build();

        var u2 = User.withUsername("emma")
                .password("12345")
                .authorities("write")
                .build();

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

        return service;
    }

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

  to define authorization rules that describe this method, you need to use the @ PreAuthorize annotation@ PreAuthorize annotation will receive a SPEL(Spring Expression Language) expression describing authorization rules as a value. This example applies a simple rule.

  you can use the hasAuthority() method to define restrictions for users according to their permissions.

3.2 service classes define pre authorization rules on methods

@Service
public class NameService {

    //Define authorization rules. Only users with write permission can call this method
    @PreAuthorize("hasAuthority('write')")
    public String getName() {
        return "Fantastico";
    }
}

3.3 controller classes that implement endpoints and use services

@RestController
public class HelloController {

    @Autowired
    private NameService nameService;

    @GetMapping("/hello")
    public String hello() {
        //Call the method for which the pre authorization rule is applied
        return "Hello, " + nameService.getName();
    }
}

3.4 testing

  start the application. We hope that only user emma has the right to call the endpoint because she has write authorization.

   call the / hello endpoint and authenticate with user emma.

   call the / hello endpoint and authenticate with user natalie.

Similarly, you can use any of the other expressions discussed earlier for endpoint validation.

hasAnyAuthority(): specify multiple permissions. The user must have at least one of these permissions to call this method.

hasRole(): Specifies the role that the user must play to call this method.

hasAnyRole(): specify multiple roles. The user must have at least one of these roles to call this method.

  next, extend this example to show how to define authorization rules using the values of method parameters.

   for this project, the same ProjectConfig class as the first example is defined here, so that the previous two users, Emma and Natalie, can continue to be used. The endpoint now gets a value through the path variable and calls a service class to get the "private name" of the given user name. Of course, in this example, the private name is just a noun referring to a feature of the user, which is not visible to everyone.

3.5 define the controller class of the test endpoint

@RestController
public class HelloController {

    @Autowired
    private NameService nameService;

    @GetMapping("/secret/names/{name}")
    public List<String> names(@PathVariable String name) {
        //Call protected endpoint
        return nameService.getSecretNames(name);
    }
}

3.6 NameService class defines protected methods

@Service
public class NameService {


    private Map<String, List<String>> secretNames = Map.of(
            "natalie", List.of("Energico", "Perfecto"),
            "emma", List.of("Fantastico"));

    //Use #name to represent the value of the method parameter in the authorization expression
    @PreAuthorize("#name == authentication.principal.username")
    public List<String> getSecretNames(String name) {

        return secretNames.get(name);
    }
}

    the expression for authorization now is = = #name = = authentication principal. username==. In this expression, we use #name to refer to the value of the getSecretNames() method parameter named name, and we can directly access the authentication object, which can be used to refer to the currently authenticated user. The expression used here indicates that the method can only be called if the user name of the authenticated user is the same as the value sent through the method parameter. In other words, users can only retrieve their own names.

3.7 testing

  start the application and test it.

  provide the value of the path variable equal to the user name

  when using emma for authentication, we try to obtain the secret name of natalie. But the call does not work:

  however, user natalie can get her secret name.

Remember that global method security can be applied to any layer of the application.

4. Post application authorization

  now, suppose you want to allow calls to methods, but in some cases, you want to ensure that the caller does not receive a return value. We need to use post authorization when we want to apply the verified authorization rules after the method call. This may sound strange: why can someone execute code without results? Of course, this has nothing to do with the method itself, but imagine that this method will retrieve some data from a data source, such as a Web service or database. We can be sure of what the method does, but we don't trust the third party who calls the method. Therefore, we should allow the method to execute, but verify what it returns. If the condition is not met, the caller is not allowed to access the return value.

  to use Spring Security post application authorization rules, you need to use the @ PostAuthorize annotation, which is similar to @ PreAuthorize. This annotation accepts the value of the SpEL that defines the authorization rule. Next, we will define the @ authorize method as an example and show how to use the @ authorize method.

4.1 enable global method security and define users

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        var service = new InMemoryUserDetailsManager();

        var u1 = User.withUsername("natalie")
                    .password("12345")
                    .authorities("read")
                    .build();

        var u2 = User.withUsername("emma")
                    .password("12345")
                    .authorities("write")
                    .build();

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

        return service;
    }

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

  you also need to declare a class to represent the Employee object with the Employee's name, book list and role list.

4.2 definition of Employee class

public class Employee {

    private String name;
    private List<String> books;
    private List<String> roles;

    public Employee(String name, List<String> books, List<String> roles) {
        this.name = name;
        this.books = books;
        this.roles = roles;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<String> getBooks() {
        return books;
    }

    public void setBooks(List<String> books) {
        this.books = books;
    }

    public List<String> getRoles() {
        return roles;
    }

    public void setRoles(List<String> roles) {
        this.roles = roles;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(name, employee.name) &&
                Objects.equals(books, employee.books) &&
                Objects.equals(roles, employee.roles);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, books, roles);
    }
}

  we may need to obtain employee details from the database. To make this example simple, we use a Map that contains some records that we treat as data sources.

4.3 define BookService class of authorized method

@Service
public class BookService {

    private Map<String, Employee> records =
            Map.of("emma",
                   new Employee("Emma Thompson",
                           List.of("Karamazov Brothers"),
                           List.of("accountant", "reader")),
                   "natalie",
                   new Employee("Natalie Parker",
                           List.of("Beautiful Paris"),
                           List.of("researcher"))
                  );
    //Defines an expression for post authorization
    @PostAuthorize("returnObject.roles.contains('reader')")
    public Employee getBookDetails(String name) {
        return records.get(name);
    }
}

The   BookService class also contains methods for applying authorization rules to it. Note that the expression used in the @ PostAuthorize annotation refers to the value returned by the returnObject method. After the method is executed, the value returned by the method can be used.

4.4 controller class to implement endpoint

   write a controller and implement an endpoint to call the method for which the authorization rules are applied.

@RestController
public class BookController {

    @Autowired
    private BookService bookService;

    @GetMapping("/book/details/{name}")
    public Employee getDetails(@PathVariable String name) {
        return bookService.getBookDetails(name);
    }
}

4.5 testing

  you can now start the application and call the endpoint to observe the behavior of the application. An example of a call endpoint is shown below. Any user can access the details of Emma because the returned role list contains the string "reader", but no user can get the details of natalie.

   call the endpoint to obtain the details of emma and authenticate with user emma.

   call the endpoint to obtain the details of emma and authenticate with user natalie.

   call the endpoint to obtain the details of natalie and authenticate with user emma.

   call the details of the endpoint natalie and authenticate with the user natalie.

Tip: if the requirement requires both pre authorization and post authorization, you can use @ PreAuthorize and @ PostAuthorize on the consent method at the same time

5. License of implementation method

  suppose the authorization logic is more complex and cannot be written in a single line of code. Verbose SpEL expressions are definitely uncomfortable. When you need to implement complex authorization rules, don't write lengthy spiel expressions, but put the logic in a separate class. Spring Security provides the concept of licensing, which makes it easy to write authorization rules in separate classes, making applications easier to read and understand.

  license application authorization rules within the project will be used here. In this scenario, there is an application that manages documents. Any document has an owner, the user who created it. To get the details of an existing document, the user must be either an administrator or the owner of the document. A license evaluator needs to be implemented to address this requirement.

5.1 Document class

public class Document {

    private String owner;

    public Document(String owner) {
        this.owner = owner;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Document document = (Document) o;
        return Objects.equals(owner, document.owner);
    }

    @Override
    public int hashCode() {
        return Objects.hash(owner);
    }
}

  in order to simulate the database and simplify the example, a repository class is created here, which will manage several document instances in the Map.

5.2 DocumentRepository class for managing several Document instances

@Repository
public class DocumentRepository {

    //Identify each document with a unique code and name the document owner
    private Map<String, Document> documents =
            Map.of("abc123", new Document("natalie"),
                    "qwe123", new Document("natalie"),
                    "asd555", new Document("emma"));


    public Document findDocument(String code) {
        //Obtain the document by using the unique identification code of the document
        return documents.get(code);
    }
}

5.3 DocumentService class that implements the protected method

@Service
public class DocumentService {

    @Autowired
    private DocumentRepository documentRepository;

    @PostAuthorize("hasPermission(returnObject, 'ROLE_admin')")
    public Document getDocument(String code) {
        //Use the hasPermission() expression to refer to the authorization expression
        return documentRepository.findDocument(code);
    }
}

   here, we need to annotate this method with @ PostAuthorize and use hasPermission() spiel expression. This method refers to the external authorization expression further implemented in this example. At the same time, please note that the parameter provided for the hey hasPermission() method is returnObject, which represents the value returned by the method and the name of the role allowed to access, that is, "ROLE_admin".

  license logic also needs to be implemented. You do this by writing an object that implements the PermissionEvaluator interface. The PermissionEvaluator interface provides two ways to implement the license logic.

  • Based on object and license: this method is used in the current example. It is assumed that the license evaluator will receive two objects: one is the object of the authorization rule, and the other will provide additional details required to implement the license logic.
  • Based on object ID, object type, and license: assuming that the license credential receives an object ID, you can use the object ID to retrieve the required object. It also receives an object type. If the same license evaluator is applied to multiple object types, it can use this type of object, and it also needs an object that provides additional information to evaluate the license.

5.4 permissionevaluator interface definition

  for the current example, the first method is sufficient. We already have an object. In our example, it is the value returned by the method. Also send the role name "Role_admin", which can access any document according to the definition of the sample scenario. Of course, in this example, you can use the name of the role directly in the license evaluator class and avoid sending it as the value of the hasPermission() object. In the actual scenario, the situation may be more complex.

   we also want to mention here that we do not need to pass the Authentication object. Spring Security will automatically provide this parameter value when calling the hasPermission() method. The framework knows the value of the Authentication instance because it is already in the SpringContext.

5.5 implementing authorization rules

@Component
public class DocumentsPermissionEvaluator
        implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication,
                                 Object target,
                                 Object permission) {
        //Cast target object to Document
        Document document = (Document) target;
        //In our example, the permission object is the role name, so we want to cast it to String
        String p = (String) permission;

        //Check whether the authenticated user has the role received as a parameter
        boolean admin =
           authentication.getAuthorities()
           .stream()
           .anyMatch(a -> a.getAuthority().equals(p));
        //This license is granted if the administrator or user authenticated by the administrator is the owner of the document.
        return admin || document.getOwner().equals(authentication.getName());
    }

    @Override
    public boolean hasPermission(Authentication authentication,
                                 Serializable targetId,
                                 String targetType,
                                 Object permission) {
        //It doesn't need to be implemented here, because the second method won't be used
        return false;
    }
}

  in order for Spring Security to know the new PermissionEvaluator implementation, a MethodSecurityExpressionHandler must be defined in the configuration class.

5.6 configure PermissionEvaluator in the configuration class

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig extends GlobalMethodSecurityConfiguration {

    @Autowired
    private DocumentsPermissionEvaluator evaluator;

    //Override the createExpressionHandler() method
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        //Define a default security expression handler to set up a custom license evaluator
        var expressionHandler =
                new DefaultMethodSecurityExpressionHandler();
        //Set up custom license evaluator
        expressionHandler.setPermissionEvaluator(evaluator);
        //Returns the custom expression handler
        return expressionHandler;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        var service = new InMemoryUserDetailsManager();

        var u1 = User.withUsername("natalie")
                    .password("12345")
                    .roles("admin")
                .build();

        var u2 = User.withUsername("emma")
                .password("12345")
                .roles("manager")
                .build();

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

        return service;
    }

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

  as for users, the only thing to pay attention to is their roles. User natalie is an administrator and can access any document. User emma is a manager who has intelligent access to his own documents.

  to test the program, define an endpoint here.

5.7 defining controllers and implementing endpoints

@RestController
public class DocumentController {

    @Autowired
    private DocumentService documentService;

    @GetMapping("/documents/{code}")
    public Document getDetails(@PathVariable String code) {
        return documentService.getDocument(code);
    }
}

5.8 testing

  user natalie can access the document regardless of the owner of the document. User emma can only access their own documents.

   call the endpoint to obtain the document belonging to natalie, and use the user natalie for authentication.

Obtain a user's endpoint   and use it for authentication.

   call the endpoint to obtain the document belonging to natalie and authenticate with user emma.

6. Summary

  • Spring Security allows us to apply authorization rules for any layer of the application, not just the endpoint layer. To do this, you need to enable global method security.
  • Global method security is disabled by default. To enable it, you can use the @ EnableGlobalMethodSecurity annotation on the configuration class of the application.
  • You can apply authorization rules that your application checks before calling a method. If these authorization rules are violated, the framework does not allow the method to be executed. When we verify authorization rules before method calls, we need to use pre authorization.
  • To achieve pre authorization, you can use the @ PreAuthorize annotation and use the value of the SpEL expression that defines the authorization rule.
  • If you want to decide whether the caller can use the return value and whether the execution process can continue only after the method call, use post authorization.
  • To implement post authorization, you need to use the @ PostAuthorize annotation and the value of the SpEL expression representing the authorization rule.
  • When implementing complex logic, you should separate the logic into another class to make the code easier to read. In Spring Security, a common way to achieve this is to implement PermissionEvaluator.
  • Spring Security provides compatibility with older specifications such as @ RolesAllowed and @ Secured annotations. We can use these annotations, but they are not as powerful as @ PreAuthorize and @ PostAuthorize, and in actual scenarios, the probability of using these annotations in spring is very small.

Keywords: Spring Spring Boot Spring Security

Added by msaz87 on Sun, 06 Mar 2022 14:21:21 +0200