05 Access Authorization How to Effectively Configure the Security Access Process for Requests

05 Access Authorization How to Effectively Configure the Security Access Process for Requests #

Through the previous lessons, I believe you already have a more comprehensive understanding of the authentication process in Spring Security. Authentication is the premise and foundation for implementing authorization. Usually, when performing authorization operations, we need to specify the target user. Only by specifying the target user can we determine the roles and permissions they possess. Users, roles, and permissions are the authorization models used in Spring Security. Today, let’s explore the implementation process of the authorization model and its application in daily development.

Permissions and Roles in Spring Security #

The basic means of implementing access authorization is through configuration methods. We have already introduced the configuration system in Spring Security in the lesson “User Authentication: How to Authenticate Users effectively with Spring Security?”. You can review and learn from it. The processing of configuration methods is also located in the WebSecurityConfigurerAdapter class, but it uses another configure(HttpSecurity http) method. The example code is as follows:

protected void configure(HttpSecurity http) throws Exception {

    http
        .authorizeRequests().anyRequest().authenticated()
        .and()
        .formLogin()
        .and()
        .httpBasic();

}

Similarly, we have already seen the above code in lesson 02, which is the default implementation method in Spring Security for access authorization.

Access Control Based on Permissions #

Let’s first review the user objects introduced in the lesson “Account System: How to Deeply Understand the Authentication Mechanism of Spring Security?”:

Drawing 0.png

Core user objects in Spring Security

The GrantedAuthority object in the above image represents a type of permission object, and a UserDetails object possesses one or more GrantedAuthority objects. Through this association relationship, we can actually restrict the user’s permissions, as shown below:

Drawing 1.png

Illustration of access control using permissions

If we express this association relationship in code, we can use the following implementation method:

UserDetails user = User.withUsername("jianxiang")
    .password("123456")
    .authorities("create", "delete")
    .build();

As you can see, we created a user named “jianxiang” who has the “create” and “delete” permissions. In Spring Security, there is a set of configuration methods specifically for GrantedAuthority. For example:

  • hasAuthority(String): Allows access for users with specific permissions.
  • hasAnyAuthority(String): Allows access for users with any of the permissions.

You can use the above two methods to determine if a user has the corresponding access permissions. We can add the following code to the configure method of WebSecurityConfigurerAdapter:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();
    http.authorizeRequests().anyRequest().hasAuthority("CREATE");
}

The purpose of this code is that for any request, only users with the “CREATE” permission can access. If we modify the code as follows:

http.authorizeRequests().anyRequest().hasAnyAuthority("CREATE", "DELETE");

At this time, users with either the “CREATE” or “DELETE” permission can access.

These two methods are relatively simple to implement, but they have limitations because we are unable to flexibly control access rules based on some environmental and business parameters. For this reason, Spring Security also provides an access() method, which allows developers to pass in an expression for more fine-grained permission control.

Here, we will introduce SpEL, which stands for Spring Expression Language. It is a dynamic expression language provided by the Spring Framework. Based on SpEL, as long as the return value of the expression is true, the access() method will allow user access. For example:

http.authorizeRequests().anyRequest().access("hasAuthority('CREATE')");

The above code has the same effect as using the hasAuthority() method. However, for more complex scenarios, the access() method has obvious advantages. We can create a flexible expression and then determine the final result through the access() method. The example code is as follows:

String expression = "hasAuthority('CREATE') and !hasAuthority('Retrieve')";
http.authorizeRequests().anyRequest().access(expression);
@RestController
public class TestController {

    @GetMapping("/hello_user")
    public String helloUser() {
        return "Hello User!";
    }

    @GetMapping("/hello_admin")
    public String helloAdmin() {
        return "Hello Admin!";
    }

    @GetMapping("/other")
    public String other() {
        return "Other!";
    }
}

The effect of the above code is that only users who have the “CREATE” permission and do not have the “Retrieve” permission can access it.

Access Control Based on Roles #

After discussing permissions, let’s take a look at roles. You can think of roles as a data carrier that has multiple permissions, as shown in the figure below. Here, we define two different roles, “User” and “Admin”, which have different permissions:

Drawing 2.png

Using roles to implement access control diagram

At this point, you may think that Spring Security should provide a separate data structure to carry the meaning of roles. However, in Spring Security, there is no defined object specifically for defining user roles, such as “GrantedRole”. Instead, it reuses the GrantedAuthority object. In fact, a GrantedAuthority with the prefix “ROLE_” represents a role, so we can initialize the user’s role in the following way:

UserDetails user = User.withUsername("jianxiang")
      .password("123456")
      .authorities("ROLE_ADMIN")
      .build();

The above code is equivalent to assigning the role “ADMIN” to the user “jianxiang”. To provide a better development experience for developers, Spring Security also provides another simplified method to specify user roles, as shown below:

UserDetails user = User.withUsername("jianxiang")
      .password("123456")
      .roles("ADMIN")
      .build();

We have seen this method when introducing the use of an in-memory user information storage scheme in the “User Authentication: How to Effectively Authenticate Users with Spring Security?” section. You can review that section for more details.

Just like permission configuration, Spring Security also uses the corresponding hasRole() and hasAnyRole() methods to determine whether a user has a specific role or roles. The usage is as follows:

http.authorizeRequests().anyRequest().hasRole("ADMIN");

Of course, for roles, we can also use the access() method to achieve more complex access control. Spring Security also provides many other useful control methods for developers to use flexibly. In summary, the table below shows common configuration methods and their functions:

Configuration Method Function
anonymous() Allows anonymous access
authenticated() Allows authenticated users to access
denyAll() Denies all access unconditionally
hasAnyAuthority(String) Allows users with any of the specified authorities to access
hasAnyRole(String) Allows users with any of the specified roles to access
hasAuthority(String) Allows users with a specific authority to access
hasIpAddress(String) Allows users from a specific IP address to access
hasRole(String) Allows users with a specific role to access
permitAll() Allows all access unconditionally

Configuration methods in Spring Security

Using Configuration Methods to Control Access Permissions #

After discussing permissions and roles, let’s return to the process of HTTP requests and responses. We know that the means of ensuring access security is to restrict access and only allow requests with the appropriate access permissions to be processed by the server. So, how to associate HTTP requests with the access control process? The answer is to use the configuration methods provided by Spring Security. Spring Security provides three powerful matchers (Matcher) to achieve this goal, namely MVC matcher, Ant matcher, and regular expression matcher.

To validate the configuration methods of these matchers, we provide a Controller as shown below:

@RestController
public class TestController {

    @GetMapping("/hello_user")
    public String helloUser() {
        return "Hello User!";
    }

    @GetMapping("/hello_admin")
    public String helloAdmin() {
        return "Hello Admin!";
    }

    @GetMapping("/other")
    public String other() {
        return "Other!";
    }
}

}

At the same time, we also create two users with different roles as follows:

UserDetails user1 = User.withUsername("jianxiang1") 
    .password("12345") 
    .roles("USER") 
    .build(); 

UserDetails user2 = User.withUsername("jianxiang2") 
    .password("12345") 
    .roles("ADMIN") 
    .build();

Next, we will explain the three different matchers based on the HTTP endpoints exposed in this Controller.

MVC Matcher #

The usage of the MVC matcher is relatively simple, just match based on the access path of the HTTP endpoint, as shown below:

http.authorizeRequests() 
    .mvcMatchers("/hello_user").hasRole("USER") 
    .mvcMatchers("/hello_admin").hasRole("ADMIN");

Now, if you use a user with the role “USER”, such as “jianxiang1”, to access the “/hello_admin” endpoint, you will get the following response:

{ 
    "status":403, 
    "error":"Forbidden", 
    "message":"Forbidden", 
    "path":"/hello_admin" 
}

Obviously, the MVC matcher has taken effect, because the “/hello_admin” endpoint can only be accessed by users with the role “ADMIN”. If you use a user with the “ADMIN” role, such as “jianxiang2”, to access this endpoint, you will get the correct response.

You may ask, we only specified the paths of these two endpoints through the MVC matcher, what about the remaining “/other” path? The answer is: Endpoints that are not matched by the MVC matcher are not limited by any restrictions, the effect is equivalent to the following configuration:

http.authorizeRequests() 
    .mvcMatchers("/hello_user").hasRole("USER") 
    .mvcMatchers("/hello_admin").hasRole("ADMIN")
    .anyRequest().permitAll();

Obviously, this type of security access control strategy is not particularly reasonable. A better approach is to control access to requests that are not matched by the MVC matcher and require authentication before they can be accessed. The implementation is as follows:

http.authorizeRequests() 
    .mvcMatchers("/hello_user").hasRole("USER") 
    .mvcMatchers("/hello_admin").hasRole("ADMIN")
    .anyRequest().authenticated();

Now, another question arises: what if a Controller has two HTTP endpoints with the same path?

This situation is possible because for HTTP endpoints, even if the paths are the same, as long as the HTTP methods used are different, they are considered two different endpoints. For this scenario, the MVC matcher also provides an overloaded mvcMatchers method, as shown below:

mvcMatchers(HttpMethod method, String... patterns)

This way, we can control the access as a dimension based on the HTTP method. Here is an example:

http.authorizeRequests() 
    .mvcMatchers(HttpMethod.POST, "/hello").authenticated() 
    .mvcMatchers(HttpMethod.GET, "/hello").permitAll() 
.anyRequest().denyAll();

In the above configuration code, if an HTTP request uses the POST method to access the “/hello” endpoint, authentication is required. Requests using the GET method to access the “/hello” endpoint are allowed. Finally, all other requests to any path will be denied.

At the same time, if we want to specify the same access control for all subpaths under a certain path, we only need to add an asterisk (*) after that path, as shown in the example code:

http.authorizeRequests() 

    .mvcMatchers(HttpMethod.GET, "/user/*").authenticated() 

With the above configuration, if we access paths like “/user/jianxiang” or “/user/jianxiang/status”, they will all match this rule.

Ant Matchers #

The Ant Matchers have a similar form and usage to the MVC Matchers introduced earlier. It also provides three methods to match requests with HTTP endpoint addresses:

  • antMatchers(String patterns)
  • antMatchers(HttpMethod method)
  • antMatchers(HttpMethod method, String patterns)

From the method definitions, it is not difficult to understand that we can specify the HTTP method of the request and the matching pattern. For example:

http.authorizeRequests() 

    .antMatchers("/hello").authenticated();

Although the Ant Matchers and MVC Matchers appear to be similar in usage, I recommend using the MVC Matchers instead of the Ant Matchers for daily development. The main reason is that the Ant Matchers have some risks in path matching, mainly when dealing with “/”. To explain this further, let me give you a simple example.

Based on the above configuration line, if you send an HTTP request like this:

http://localhost:8080/hello

You would certainly expect the Ant Matchers to match this endpoint, but the result is:

{
    "status":401, 
    "error":"Unauthorized", 
    "message":"Unauthorized", 
    "path":"/hello" 
}

Now, if you adjust the HTTP request like this, please note that we added a “/” symbol at the end of the request address, then you will get the correct result:

http://localhost:8080/hello/

Clearly, the way the Ant Matchers handle the request address can be confusing, while the MVC Matchers do not have this issue and can perform correct matching regardless of whether there is a “/” symbol at the end of the request address.

Regular Expression Matchers #

Finally, I want to introduce the Regular Expression Matchers, which also provide two configuration methods:

  • regexMatchers(HttpMethod method, String regex)
  • regexMatchers(String regex)

The main advantage of using this matcher is that it allows matching the request address based on complex regular expressions, which cannot be achieved by the MVC Matchers and Ant Matchers. Take a look at the following configuration code:

http.authorizeRequests()
   .mvcMatchers("/email/{email:.*(.+@.+\\.com)}")
   .permitAll()
   .anyRequest()
   .denyAll();

As you can see, this code matches common email addresses and allows access only if the input request is a valid email address.

Summary and Preview #

In this lesson, we focused on authorizing requests, which requires understanding the relationship between users, authorities, and roles in Spring Security. Once we have set the corresponding authorities and roles for a user, we can use various configuration methods to effectively control access permissions. For this purpose, Spring Security provides MVC Matchers, Ant Matchers, and Regular Expression Matchers to implement complex access control.

Here is a summary of this lesson:

Drawing 3.png

Finally, I would like to leave you with a question: In Spring Security, do you know the difference and connection between user roles and user authorities? Feel free to share your views in the comments section.