19 Service Authorization How to Guarantee Request Security Access Based on Spring Security

19 Service Authorization - How to Guarantee Request Security Access based on Spring Security #

In Lesson 18, we focused on how to build a user authentication system using WebSecurityConfigurerAdapter. In this lesson, we will continue using this configuration class to control authorization for service access.

In everyday development, we need to control different HTTP endpoints in a web application with different levels of granularity, and we want this control method to be flexible enough. With the help of the Spring Security framework, we can easily achieve this. Let’s take a look together.

Managing Authorization for HTTP Endpoints #

In a web application, the objects managed by authorization are the individual HTTP endpoints exposed by the Controller layer, and these HTTP endpoints are the resources that require authorized access.

Developers can use a series of rich technology components provided by Spring Security to flexibly manage authorization through simple configuration.

Using configuration methods #

The first method to implement authorization is to use configuration methods, which are also located in the WebSecurityConfigurerAdapter class, but are used with the configure(HttpSecurity http) method, as shown in the code below:

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .and()
        .httpBasic();
}

The above code is the default implementation method of authorization in Spring Security, which uses several common configuration methods.

Recalling the content of Lesson 18, once the Spring Security framework is introduced into the classpath, a login page will pop up when accessing any endpoint, completing user authentication. Authentication is the prerequisite for authorization, and after authentication is completed, the authorization process begins.

Combining the names of these configuration methods, let’s briefly analyze the specific steps to implement this default authorization effect.

First, with the authorizeRequests() method of the HttpSecurity class, we can restrict all HttpServletRequests for accessing HTTP endpoints.

Second, the statement anyRequest().authenticated() specifies that all requests need to be authenticated, which means that users who have not been authenticated cannot access any endpoints.

Next, the formLogin() statement specifies that users need to use a form to log in, which means a login page will pop up.

Finally, the httpBasic() statement uses the Basic Authentication method in the HTTP protocol to complete authentication.

In Lesson 18, we also demonstrated how to use Postman to complete authentication, so we won’t go into too much detail here.

Of course, Spring Security also provides many other useful configuration methods for developers to use flexibly. In the table below, we list some of these methods.

Lark20210119-172757.png

Based on the configuration methods in the table above, we can implement custom authorization strategies using HttpSecurity.

For example, if we want to control access to all endpoints under the “/orders” root path and only allow authenticated users to access them, we can create a SpringCssSecurityConfig class that inherits from the WebSecurityConfigurerAdapter class and override the configure(HttpSecurity http) method, as shown in the code below:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/orders/**")
            .authenticated();
    }
}

Please note: Although these configuration methods in the table above are very useful, they have certain limitations because we cannot flexibly control access rules based on environmental and business parameters.

To address this, Spring Security also provides an access() method, which allows developers to pass in an expression for more fine-grained permission control. Here, we introduce a dynamic expression language provided by the Spring framework called SpEL (Spring Expression Language). As long as the return value of the SpEL expression is true, the access() method allows user access, as shown in the code below:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/orders")
        .access("hasRole('ROLE_USER')");
}

In the above code, assuming that requests to the “/orders” endpoint must have the “ROLE_USER” role, we can flexibly implement this requirement using the hasRole method in the access method. Of course, in addition to using hasRole, we can also use expressions such as authentication, isAnonymous, isAuthenticated, and permitAll to achieve the same functionality. Since the purpose of these expressions is consistent with the configuration methods mentioned earlier, we will not go into too much detail.

Using annotations #

In addition to using configuration methods, Spring Security also provides the @PreAuthorize annotation to achieve similar effects. The definition of this annotation is as follows:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
    // Set access control using SpEL expressions
    String value();
}

As you can see, the principle of @PreAuthorize is the same as the access() method introduced earlier, which means that access control is set by passing in a SpEL expression. The following code is a typical example of this:

@RestController
@RequestMapping(value="orders")
public class OrderController {
    @PostMapping(value = "/")
    @PreAuthorize("hasRole(ROLE_ADMIN)")
    public void addOrder(@RequestBody Order order) {
        ...
    }
}

From this example, we can see that on the “/orders/” HTTP endpoint, we added a @PreAuthorize annotation to restrict access to only users with the role “ROLE_ADMIN”.

In fact, there is another annotation in Spring Security for authorization called @PostAuthorize. It is a pair with the @PreAuthorize annotation and is mainly used to check permissions after the request is completed. Since this situation is relatively rare, we will not go into further details here. You can refer to relevant materials for further learning.

Implementing a multi-dimensional access control authorization solution #

We know that an HTTP endpoint is a resource of a web application, and the level of protection for each web application resource varies depending on the service. For general HTTP endpoints, users may only need to be authenticated to access the resources; for some important HTTP endpoints, there may be additional requirements on top of authentication.

Next, we will discuss three levels of granularity for protecting resources.

  • User-level: This level is the most basic level of resource protection, where any authenticated user may access various resources within the service.
  • User + Role level: On top of the authenticated user level, this level also requires users to belong to one or more specific roles.
  • User + Role + Operation level: On top of the authenticated user + role level, this level also restricts access to certain HTTP methods.

Based on configuration methods and annotations, we can easily implement these three access control authorization scenarios.

Protecting service access at the user level #

This time, let’s go to the customer-service in the SpringCSS sample system and review the content of the CustomerController, as shown below:

@RestController
@RequestMapping(value="customers")
public class CustomerController {

    @Autowired
    private CustomerTicketService customerTicketService;

    @PostMapping(value = "/{accountId}/{orderNumber}")
    public CustomerTicket generateCustomerTicket(@PathVariable("accountId") Long accountId,
            @PathVariable("orderNumber") String orderNumber) {

        CustomerTicket customerTicket = customerTicketService.generateCustomerTicket(accountId, orderNumber);
        return customerTicket;
    }

    @GetMapping(value = "/{id}")
    public CustomerTicket getCustomerTicketById(@PathVariable Long id) {

        CustomerTicket customerTicket = customerTicketService.getCustomerTicketById(id);
        return customerTicket;
    }

    @GetMapping(value = "/{pageIndex}/{pageSize}")
    public List<CustomerTicket> getCustomerTicketList(@PathVariable("pageIndex") int pageIndex, @PathVariable("pageSize") int pageSize) {

        List<CustomerTicket> customerTickets = customerTicketService.getCustomerTickets(pageIndex, pageSize);
        return customerTickets;
    }

    @DeleteMapping(value = "/{id}")
    public void deleteCustomerTicket(@PathVariable("id") Long id) {

        customerTicketService.deleteCustomerTicket(id);
    }
}

Because the CustomerController is the core entry point in the SpringCSS example, we believe that all its endpoints should be protected. Therefore, in the customer-service module, we create a SpringCssSecurityConfig class that extends WebSecurityConfigurerAdapter, as shown in the following code:

@Configuration
public class SpringCssSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
    
        http.authorizeRequests()
            .anyRequest()
            .authenticated();
    }
}

The .anyRequest().authenticated() statement in the configure() method specifies that any request to any endpoint under /customer-service requires authentication. Therefore, when we access any URL in the CustomerController using a regular HTTP request (e.g. http://localhost:8083/customers/1), we will receive an error message as shown in the following code, which explicitly states that full authentication is required to access the resource.

{
    "error": "access_denied",
    "error_description": "Full authentication is required to access to this resource"
}

In Lecture 18, when we override the config(AuthenticationManagerBuilder auth) method of WebSecurityConfigurerAdapter, we provided a username “springcss_user”. Now we will use this username to add user authentication information and access this endpoint again. Obviously, because we are now providing valid user information, the authentication requirements can be met.

Accessing with User + Role Level Protection #

For some HTTP endpoints with high security requirements, we usually need to restrict access to certain roles.

For example, the customer-service module involves core business operations such as customer ticket management. We believe that resource access should not be open to all authenticated users, but only to administrators with the role “ADMIN”. In this case, we can use the access control mechanism of user authentication + role to protect service access. The specific code example is as follows:

@Configuration
public class SpringCssSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
            .antMatchers("/customers/**")
            .hasRole("ADMIN")
            .anyRequest()
            .authenticated();
    }
}

In the above code, we used the antMatchers("/customer/**") and hasRole("ADMIN") methods of the HttpSecurity class to restrict requests to /customers/** to the role “ADMIN”. Only authenticated users with the “ADMIN” role can access all URLs starting with “/customers/”.

If we protect service access using user authentication + role, when a user with the role “USER” and the username “springcss_user” accesses the customer-service, an “access_denied” error message will appear as shown below:

{
    "error": "access_denied",
    "error_description": "Access is denied"
}

If we access the customer-service with the user “springcss_admin” having the “ADMIN” role, we will receive a normal response. You can try this for yourself.

Accessing with User + Role + Operation Level Protection #

The last strategy for protecting service access has the finest granularity. On top of user authentication + role, we need to further restrict specific HTTP operations.

In the customer-service, we consider all delete operations on customer tickets to be dangerous. Therefore, we can use the http.antMatchers(HttpMethod.DELETE, "/customers/**") method to protect the deletion operation. The code example is as follows:

@Configuration
public class SpringCssSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
            .antMatchers(HttpMethod.DELETE, "/customers/**")
            .hasRole("ADMIN")
            .anyRequest()
            .authenticated();
    }
}

The above code protects the “/customers” endpoint for the delete operation. We need to use the user with the role “ADMIN” and the username “springcss_admin”. For other operations, it is not required. If we perform the delete operation with the “springcss_user” account, the “access_denied” error message will still appear.

Summary and Preview #

Through Lecture 19, we have clarified the three granularity levels for access authorization control in web applications and provided implementation examples for each level based on the SpringCSS project.