18 Tech Trends How to Add Reactive Programming Features to Spring Security

For most daily business scenarios, software systems need to ensure instant responsiveness at all times. Reactive programming is a new programming technique used to build systems with instant responsiveness. With the release of Spring 5, we ushered in a new era of reactive programming. As a member of the Spring family, Spring Security has also implemented a series of reactive components. Today, we will discuss these components.

What is Reactive Programming? #

Before introducing reactive Spring Security, let’s first understand some basic concepts of reactive programming and the reactive programming components integrated in Spring 5.

Basic Concepts of Reactive Programming #

In a reactive system, any operation can be seen as an event, and these events form a data flow. This data flow is a comprehensive concept for the technical stack. In other words, the entire data transmission chain, from the underlying database to the service layer and finally to the web layer, including any intermediate layer component, should operate in an event-driven manner. This way, we can handle data without using traditional synchronous call methods and let the components located upstream from the database automatically execute events. This is the core feature of reactive programming.

Specific operations on data flows are defined in the Reactive Stream specification. In the Java world, there are several mainstream open-source frameworks that implement the Reactive Stream specification, including RxJava, Vert.x, and Project Reactor.

Project Reactor #

Spring 5 has chosen Project Reactor as its built-in reactive programming framework. This framework provides two ways to represent data flows: Flux, which represents an asynchronous sequence of 0 to n elements, and Mono, which represents 0 or 1 element. We can create a Flux object with a simple code example, as shown below:

private Flux<Order> getAccounts() {

        List<Account> accountList = new ArrayList<>();

 

        Account account = new Account();

        account.setId(1L);

        account.setAccountCode("DemoCode");

        account.setAccountName("DemoName");

        accountList.add(account);

                

        return Flux.fromIterable(accountList);

}

In the above code, we use the Flux.fromIterable() method to construct a Flux object and return it. Flux.fromIterable() is a common method for constructing Flux. The Mono component also provides a set of useful methods to create Mono data flows, for example:

private Mono<Account> getAccountById(Long id) {   

        Account account = new Account();

        account.setId(id);

        account.setAccountCode("DemoCode");

        account.setAccountName("DemoName");

        accountList.add(account);

           

        return Mono.just(account);

}

As you can see, we first build an Account object, and then return a Mono object using the Mono.just() method.

Spring WebFlux #

In addition, for the complete application development process, Spring 5 also provides specific frameworks for the web layer (WebFlux) and the data access layer (Spring Data Reactive). Since Spring Security is mainly used for web applications, let’s discuss WebFlux in more detail here.

To use WebFlux in Spring Boot, you need to add the following dependency:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-webflux</artifactId>

</dependency>  

Please note that spring-boot-starter-webflux is the foundation for developing reactive web applications. Developers have two choices to build reactive web services based on the WebFlux programming model. The first choice is to use the Java annotation-based approach, and the second choice is to use the functional programming model. Among them, the Java annotation-based approach is identical to using Spring MVC. Let’s take a look at an example:

@RestController

public class HelloController {

 

    @GetMapping("/")
        return Mono.error(new UnsupportedOperationException("Updating password is not supported"));
    }
}

上述代码展示了响应式 Spring Security 中的一个响应式版本的 UserDetailsService的实现类MapReactiveUserDetailsService。它提供了基于内存的用户信息存储方案。在该实现类中,findByUsername()方法返回的是一个Mono对象,该方法通过username获取用户信息。如果找不到对应的用户信息,则返回一个空的Mono对象。updatePassword()方法则抛出一个不支持的操作异常。

值得一提的是,响应式 Spring Security 还提供了 ReactiveUserDetailsPasswordService 接口,它允许修改用户密码。在上述代码中,MapReactiveUserDetailsService 实现了该接口并抛出一个不支持的操作异常,表示不支持修改密码的操作。

通过引入响应式编程技术,Spring Security 在处理用户认证方面带来了一些变化,使其支持响应式的数据访问和操作。

return Mono.just(user)
    .map(u ->
        User.withUserDetails(u)
            .password(newPassword)
            .build()
    )
    .doOnNext(u -> {
        String key = getKey(user.getUsername());
        this.users.put(key, u);
    });
}

private String getKey(String username) {
    return username.toLowerCase();
}

From the above code, we can see that a Map is used to store user information, and then in the findByUsername() method, it returns a Mono object through the Mono.just() method. We also notice the use of the map() method in the updatePassword() method, which is actually an operator provided by Project Reactor that performs mapping operations on an object.

Based on MapReactiveUserDetailsService, we can build a ReactiveUserDetailsService in the business system as follows:

@Bean
public ReactiveUserDetailsService userDetailsService() {
    UserDetails u = User.withUsername("john")
            .password("12345")
            .authorities("read")
            .build();

    ReactiveUserDetailsService uds = new MapReactiveUserDetailsService(u);

    return uds;
}

Of course, for user authentication, Reactive Spring Security also provides a reactive version of ReactiveAuthenticationManager to perform the actual authentication process.

Reactive Authorization Mechanism #

After introducing authentication, let’s take a look at authorization. Assuming there is a simple HTTP endpoint in the system:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public Mono<String> hello(Mono<Authentication> auth) {
        Mono<String> message = auth.map(a -> "Hello " + a.getName());
        return message;
    }
}

Here, we use Spring Webflux to build a reactive endpoint, and notice that the return value of hello() is a Mono object. At the same time, the input is also a Mono object, so it is obvious that access to this endpoint requires authentication.

In Lesson 10, we learned that access permissions can be set by overriding the configure(HttpSecurity http) method in WebSecurityConfigurerAdapter. This configuration method cannot be used in the reactive programming model, and instead, a configuration interface called SecurityWebFilterChain is used to complete the configuration. The interface is defined as follows:

public interface SecurityWebFilterChain {
    Mono<Boolean> matches(ServerWebExchange exchange);
    Flux<WebFilter> getWebFilters();
}

From the naming, we can understand that SecurityWebFilterChain actually represents a filter chain, and ServerWebExchange is an interaction context that contains the request and response. This is a fixed attribute in the reactive environment, because we consider the entire interaction process not only as sending requests and receiving responses, but as exchanging data. To use SecurityWebFilterChain, you can use code similar to the following example:

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http.authorizeExchange()
            .pathMatchers(HttpMethod.GET, "/hello").authenticated()
            .anyExchange().permitAll()
                .and()
            .httpBasic()
                .and()
            .build();
}

Here, ServerHttpSecurity can be used to build an instance of SecurityWebFilterChain, which is similar to HttpSecurity used in non-reactive systems. At the same time, ServerHttpSecurity also provides a set of familiar configuration methods to set various authentication and authorization mechanisms.

Note that in the reactive system, because the object being processed is ServerWebExchange, not the traditional ServerRequest, all method names related to the request have been uniformly adjusted. For example, the authorizeRequests() method is replaced by authorizeExchange(), the anyRequest() method is replaced by anyExchange(), and the pathMatchers() method here can also be equivalent to the mvcMatchers() method introduced before.

Reactive Method-Level Access Control #

In Lesson 10, we introduced a powerful feature provided by Spring Security, called global method security. With this mechanism, authorization rules can be applied based on the execution process of methods, whether it is a web service or a general application.

In the reactive programming model, we refer to this method-level authorization mechanism as Reactive Method Security to distinguish it from traditional global method security.

To use reactive method security in an application, we also need to introduce a new annotation, @EnableReactiveMethodSecurity. This annotation is similar to @EnableGlobalMethodSecurity and is used to enable reactive method security:

@Configuration
@EnableGlobalMethodSecurity
public class SecurityConfig

Now, let’s take a look at an example of using reactive method security:

@RestController
public class HelloController {

    @GetMapping("/hello")
    @PreAuthorize("hasRole('ADMIN')")
    public Mono<String> hello() {
        return Mono.just("Hello!");
    }
}

As we can see, the @PreAuthorize annotation is used here, and the role-based authorization mechanism is implemented using the hasRole('ADMIN') SpEL expression. In terms of the usage of this annotation, we can see that it is consistent with the usage in traditional applications. Unfortunately, as it stands, the reactive method security mechanism is not very mature and only provides the @PreAuthorize and @PostAuthorize annotations, while the @PreFilter and @PostFilter annotations have not been implemented yet.

Summary and Preview #

Reactive programming is a trend in technology development and provides a new programming model for building highly resilient applications. As an important part of the Spring family, the Spring Security framework also provides comprehensive support for reactive programming. This lesson has elaborated on the basic concepts of reactive programming and provided reactive solutions for user authentication, authorization mechanisms, and method-level access control in Spring Security.

Here’s a question for you to think about: What are the differences between reactive programming and traditional programming when implementing authorization mechanisms?