Spring Security in Action Chapter 7 Configuring Authorization: Restricting Access

This column will start from the basics, step by step, and use actual combat as a clue to gradually deepen the knowledge related to SpringSecurity, create a complete SpringSecurity learning step, improve engineering coding ability and thinking ability, and write high-quality code. I hope everyone can gain something from it, and please support me.
Column address:SpringSecurity column
The code involved in this article has been placed on gitee:gitee address
If there are any mistakes in the knowledge points of the article, please correct them! Let’s learn together and make progress together.
Column summary:Column summary


This chapter includes

  • Define permissions and roles
  • Apply authorization rules on the controller

A few years ago, while skiing in the beautiful Carpathian Mountains, I witnessed an interesting scene. There was a group of people lining up to get into the cabin and get ready to go to the top of the ski slopes. A well-known internet celebrity showed up accompanied by two bodyguards. He stepped forward confidently, expecting to use his celebrity status to skip the line. Walking to the front of the line, he got a surprise. "Please show your ticket! "The person in charge of boarding said: "First of all, you need a ticket. Secondly, there is no priority queue for boarding this time. Sorry, please stand in the back queue." "He pointed to the end of the line. As with most things in life, it doesn't matter who you are. The same is true for programs. When trying to access a specific function or data, it doesn't matter who you are!

So far, we have only discussed authentication, which, as you know, is the process by which an application identifies the caller of a resource. In the examples in the previous chapters, we did not implement any rules to decide whether to approve a request. We only care about whether the system knows the user. In most applications, not one user can access every resource in the system. In this chapter, we will discuss delegation. Authorization is the process by which the system determines whether an identified client has permission to access the requested resource.

img
Figure 7.1 Authorization is the process by which an application decides whether to allow an authenticated entity to access a resource. Authorization always occurs after authentication.

In Spring Security, once the application ends the authentication process, it delegates the request to the authorization filter. This filter allows or denies the request based on configured authorization rules (Figure 7.2).

image-20230119145419680

Figure 7.2 When the client makes a request, the authentication filter authenticates the user. After successful authentication, the authentication filter stores the user's details in the security context and forwards the request to the authorization filter. Authorization filters determine whether the call is allowed. To decide whether to authorize the request, the authorization filter uses details from the security context.

7.1 Access restrictions based on permissions and roles

In this section, you will learn about the concepts of authorization and roles, and use these to secure all APIs of your application. Only by understanding these concepts can you then apply them in real-world scenarios where different users have different permissions. Depending on the permissions a user has, they can only perform a specific action.

In Chapter 3, we implemented the GrantedAuthority interface. We did not use GrantedAuthority at that time. This interface was mainly related to the authorization process. Now we can return to GrantedAuthority and examine its purpose. Figure 7.3 shows the relationship between the conventions of the UserDetails interface and the GrantedAuthority interface. Once we've discussed this interface, we'll learn how to use these rules individually or for specific requests.

image-20230119151303574

Figure 7.3 A user has one or more permissions (actions the user can do). During the authentication process, UserDetailsService obtains all details about the user, including permissions. After the application successfully authenticates the user, it uses the permissions represented by the GrantedAuthority interface for authorization.

Listing 7.1 shows the definition of the GrantedAuthority interface. Each authorization represents the user's permissions to operate on a series of program resources. Each authority has a corresponding name, which is returned as a string by the object's getAuthority() action. Typically, an authorization rule would look like this: "Allow Jane to delete product records," or "Allow John to read document records." In these cases, delete and read are the permissions granted. The application allows users Jane and John to perform these operations, usually named read, write, or delete.

Code Listing 7.1 GrantedAuthority interface The GrantedAuthority contract

public interface GrantedAuthority extends Serializable {
    
    
	String getAuthority();
}

UserDetails is the interface for describing users in Spring Security. It has a collection of GrantedAuthority instances, as shown in Figure 7.3. You can allow a user to have one or more permissions. The getAuthorities() method returns a collection of GrantedAuthority instances. In code listing 7.2, we can view this method in the UserDetails interface. We can implement this method so that it returns all permissions granted to the user. After authentication, these authorizations are part of the details about the logged in user that applications can use to grant permissions.

Code Listing 7.2 getAuthorities(method from UserDetails interface

public interface UserDetails extends Serializable {
    
    
    Collection<? extends GrantedAuthority> getAuthorities();
    // 剩余代码省略
}

7.1.1 Restrict API access based on user permissions

In this section, we will discuss restricting access to the API to specific users. In our case so far, any authenticated user can call any of the application's APIs. From now on, you'll learn how to customize this access. We will write a few examples to give you an idea of ​​the various ways to apply these restrictions with Spring Security.

image-20230119152522042

Figure 7.4 Authorizations are the actions a user can perform within an application. Based on these operations, you can implement authorization rules. Only users with specific permissions can make specific requests to an endpoint. For example, Jane can only read and write endpoints, while John can read, write, delete, and update endpoints.

Now that we have understood the UserDetails and GrantedAuthority interfaces and the relationship between them, it is time to write a small program that applies authorization rules. Through this example, we can learn some alternatives to configure access to the terminal based on the user's permissions. Let's start a new project, which I named ch07-001-authorization. Here are three methods you can use to configure the mentioned APIs:

  • hasAuthority() Only users with this authority can call this API.
  • hasAnyAuthority() can receive more than one authority. The user must have at least one of the specified permissions to access the request.

I recommend using this method or the hasAuthority() method because they are simple, depending on the number of permissions you assign to the user. These are simple read configurations that make our code easier to understand.

  • access() provides endless possibilities for configuring access permissions because the application is based on Spring Expression Language (SpEL) to build authorization rules. However, it makes the code harder to read and debug. For this reason, I recommend it as a smaller solution and only if you cannot apply the hasAnyAuthority() or hasAuthority() methods.

The only dependencies in the pom.xml file are spring-boot-starter-web and spring-boot-starter-security. These dependencies are close enough to all three of the previously listed solutions. You can find this example in project ch07-001-authorization.

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

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

We also added an API to the application to test our authorization configuration.

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    
    
    @GetMapping("/hello")
    public String hello(){
    
    

        return "Hello!";
    }
}

In a configuration class, we declare an InMemoryUserDetailsManager as our UserDetailsService and add two users, John and Jane, to be managed by this instance. Each user has different permissions. You can see how to do this in the list below.

Code Listing 7.3 Declare UserDetailsService and assign users

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;


@Configuration
public class ProjectConfig {
    
    
    @Bean
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        UserDetails user1 = User.withUsername("john").password("12345").authorities("READ").build();
        UserDetails user2 = User.withUsername("jane").password("12345").authorities("WRITE").build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

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

image-20230119154528588

The next thing to do is to add authorization configuration. In Chapter 2, we configured how to make all APIs accessible to everyone. To do this, we extend the WebSecurityConfigurerAdapter class and overload the configure() method, as shown in Code Listing 7.4:

Listing 7.4 gives everyone access to all endpoints without authentication

image-20230119155003820

The authorizeRequests() method allows us to specify authorization rules on the API. The anyRequest() method indicates that the rule applies to all requests, regardless of the URL or HTTP method used. The permitAll() method allows access to all requests, whether authenticated or not.

Let's say we want to ensure that only users with WRITE permission can access all endpoints. For our example, this means that only Jane has access. This time we can achieve our goal of restricting access based on the user's permissions. Take a look at the code in the list below.

Listing 7.5 Restricting access to only users with WRITE permissions

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    //省略部分代码

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests()
                .anyRequest()
                .hasAuthority("WRITE");  //限制只有 WRITE 权限的用户可以访问
    }
}

As you can see, the hasAuthority() method is used here to replace the permitAll() method, and the permission name that the user is allowed to use is given here as a parameter of the hasAuthority() method. The application first needs to authenticate the request and then, based on the user's permissions, decide whether to allow the call.

Next, test the application and call the API using different users. When we call the api with user Jane, the HTTP response status is 200 OK, and the response body we see is "Hello!"; when we call with user John, the HTTP response status is 403 Forbidden, and we get an empty response. body.

curl -u jane:12345 http://localhost:8080/hello
Hello!
curl -u john:12345 http://localhost:8080/hello
{
    
    
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/hello"
}

In a similar way, you can also use the hasAnyAuthority() method, which can receive multiple permission names, indicating that as long as the user has any permissions, he or she can access certain APIs.

You can replace the previous hasAuthority() with hasAnyAuthority("WRITE"), in which case the program works in the same way. However, if you replace hasAuthority() with hasAnyAuthority("WRITE", "READ"), then requests from users with both permissions will be accepted. In our example, the application allows requests from John and Jane. In the following list, you can see how to apply the hasAnyAuthority() method.

Code Listing 7.6 Applying the hasAnyAuthority() method

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;


@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345").
                authorities("READ").
                build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .authorities("WRITE")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();

        //允许具有WRITE或READ权限的用户访问
        http.authorizeRequests()
                .anyRequest()
                .hasAnyAuthority("WRITE","READ");
    }
}

To specify access based on user permissions, the third way is the access() method. However, the access() method is more general. It receives a Spring expression (SpEL) that specifies authorization conditions as a parameter. This approach is powerful, and it’s about more than just empowerment. However, this approach also makes the code harder to read and understand. For this reason, I recommend it as a last resort, and only if you cannot apply one of the hasAuthority() or hasAnyAuthority() methods described earlier in this section.

To make this method easier to understand, I first present it as an alternative to specifying permissions with the hasAuthority() and hasAnyAuthority() methods. The method must provide a Spring expression as a method parameter. However, the advantage of the access() method is that it allows you to customize the rules by providing an expression as a parameter.

Note In most cases, the required restrictions can be achieved using the hasAuthority() and hasAnyAuthority() methods, and their use is recommended. Only use the access() method if the other two options are not suitable and you want to implement more general authorization rules.

We start with a simple example to match the same requirements as in the previous case. If you only need to test whether the user has specific permissions, the expression that needs to be used with the access() method can be one of the following.

  • hasAuthority(‘WRITE’) - Prompts the user that WRITE authorization is required to call the endpoint.
  • hasAnyAuthority(‘READ’, ‘WRITE’)-Specifies that the user requires either READ or WRITE authority. With this expression, you can enumerate all the permissions you want to allow access to.

Note that the names of these expressions are the same as the methods introduced earlier in this section. The following code demonstrates how to use the access() method.

Code Listing 7.7 Use the access() method to configure access to the API

image-20230119162909952

You can see from the example in Listing 7.7 how the access() method complicates the syntax if you use it for direct requirements. In this case, the hasAuthority() or hasAnyAuthority() method should be used directly. But the access() method is not entirely undesirable. As mentioned earlier, it provides you with flexibility. In real-world scenarios, you can use it to write more complex expressions based on which the application grants access. Without the access() method, you cannot implement these scenarios.

In code listing 7.8, we found that if the access() method does not apply expressions, it is not easy to write such permission control. To be precise, the configuration in Listing 7.8 defines two users, John and Jane, who have different permissions. User John only has read permissions, while Jane has read, write and delete permissions. The API should be accessed by users with read permissions, not those with delete permissions.

Code Listing 7.8 Using the access() method with a more complex expression

image-20230119163317445

Of course, this is just a hypothetical example, but it's simple enough to understand and complex enough to demonstrate why the access() method is more powerful.

7.1.2 Restrict access to all APIs based on user roles

In this section, we will discuss restricting access to the API based on roles. Roles are another way of referring to what a user can do (Figure 7.5). You'll find these in real-world applications as well, so that's why it's important to understand roles and the difference between roles and permissions. In this section, we'll apply a few examples of using roles so that you know all the real-life situations where applications use roles, and how to write configurations for those situations.

image-20230119164523120

Figure 7.5 Roles are coarse-grained. Each user with a specific role can only perform actions granted by that role. When applying this philosophy to authorization, requests are allowed based on the user's purpose in the system. Only users with specific roles can call an API.

Spring Security understands permissions as fine-grained privileges and imposes restrictions on them. A role gives a user permissions for a set of actions. For example, in your program, a user may have either only read permissions, or all: read, write, and delete permissions. In this case, if you consider that those users who can only read have a role called READER, and other users have the role of ADMIN, having the ADMIN role means that the application grants you read, write, update, and delete permissions. There may be more roles in the program. For example, if at some point you need a user that only allows reading and writing, you can create a third role for your application named MANAGER.

Note When using methods with roles in an application, we will no longer have to define permissions. But in an application, a role needs to be defined to cover the actions to which one or more users are authorized.

Users can customize the name of the role. Compared with authorization, roles are coarse-grained and one role contains multiple authorizations. When defining a role, its name should begin with the prefix ROLLE_. At an implementation level, this prefix specifies the difference between roles and permissions. In the next code listing, take a look at the changes I made to the previous example.

Listing 7.9 Setting roles for users

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .authorities("ROLE_ADMIN")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .authorities("ROLE_MANAGER")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

   //省略其他代码
}

To set constraints for a user role, you can use one of the following methods.

  • hasRole() - Receives one parameter, which is the role name of the application authorization request.
  • hasAnyRole() - Receives as argument the name of the role for which the application approves the request.
  • access() - Use Spring expressions to specify one or more roles for application authorization requests. In terms of roles, you can use hasRole() or hasAnyRole() as a SpEL expression.

As you can see, the names are similar to the methods introduced in Section 7.1.1. In the next code listing, you can see what the configure() method looks like now.

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;


@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .authorities("ROLE_ADMIN")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .authorities("ROLE_MANAGER")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();

        //hasRole()方法现在指定了允许访问该api的角色。注意,ROLLE_前缀不会出现在这里。
        http.authorizeRequests()
                .anyRequest().hasRole("ADMIN");
    }
}

Note: It is important to note that we only use the ROLE_ prefix to declare roles. But when we use a character, we just do it by its name.

The test results are as follows:

curl -u john:12345 http://localhost:8080/hello
Hello!
curl -u jane:12345 http://localhost:8080/hello
{
    
    
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/hello"
}

Note: Make sure that the parameters you provide to the role() method do not include the ROLLE_ prefix. If you accidentally include this prefix in the role() parameter, this method will throw an exception. In short, when using the authorities() method, include the ROLLE_ prefix. When using the role() method, do not include the ROLLE_ prefix.

In the following code listing, you can see the correct use of the role() method instead of the authorities() method when designing role-based access.

Code Listing 7.11 Setting the role using the role() method

image-20230119170707960

In Sections 7.1.1 and 7.1.2, we learned how to use the access() method to apply authorization rules mentioning permissions and roles. Generally speaking, in an application, authorization restrictions are related to permissions and roles. But it's important to remember that the access() method is universal. In the example I presented, I was mainly teaching you how to apply it to permissions and roles, but in practice it can accept any SpEL expression. It does not need to be associated with permissions and roles. A straightforward example would be to configure access to an endpoint to only be allowed after 12:00 PM. To solve a problem like this, you can use the following SpEL expression:

T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(12, 0))

For more information about SpEL expressions, see the Spring documentation:Core Technologies (spring.io)

Through the access() method, basically any kind of rule can be implemented. The possibilities are endless. Just don't forget that in applications we always try to make the syntax as simple as possible. It only complicates your configuration if you have no other options.

7.1.3 Restrict access to all APIs

In this section, we will discuss restricting access to all requests. We learned in Chapter 5 that access to all requests can be allowed by using the permitAll() method. You also learned that you can apply access rules based on permissions and roles, as well as deny all requests. The denyAll() method is exactly the opposite of the permitAll() method. In the following code listing, you can see how to use the denyAll() method.

Code Listing 7.12 Using the denyAll( method to restrict access to endpoints

image-20230119171349472

So where can this restriction be used? Suppose there is an API that takes an email address as an input parameter. We want to allow requests whose variable address value ends in .com. We do not want the application to accept email addresses in any other format. For this requirement, we can use a regular expression to group requests that match the rule, and then use the denyAll() method to cause the application to deny all these requests (Figure 7.6).

image-20230119171722316

Figure 7.6 When the user calls the endpoint and provides a parameter value ending in .com, the application accepts the request. When the user calls the endpoint and provides an email address ending in .net, the application rejects the call. To achieve this behavior, you can use the denyAll() method on all endpoints whose parameter values ​​do not end in .com.

In the microservice scenario, as shown in Figure 7.7, different microservices have different functions, and these use cases can be accessed by calling APIs on different paths. But in order to call an API, you need to request the gateway. In this architecture, there are two gateway services. In Figure 7.7, I call them Gateway A and Gateway B. If the client wants to access the /product path, it requests gateway A. But for the /article path, the client must request gateway B. Each gateway service is designed to deny all requests for other paths that the service does not serve. This simplified scenario can help you understand the denyAll() method easily. In a production application, you can find similar situations in more complex architectures.

image-20230119172207660

Figure 7.7 Access through gateway A and gateway B. Each gateway only accepts requests for specific paths and rejects all other requests.

Summarize

  • Authorization is the process by which an application determines whether an authorized request is allowed. Authorization always occurs after authorization.
  • In your application, you can specify that certain requests are accessible to unauthenticated users.
  • The application can be configured to deny any request using the denyAll() method, or to allow any request using the permitAll() method.

Guess you like

Origin blog.csdn.net/Learning_xzj/article/details/128736633