保护反应式Web API

自Spring Security诞生以来(甚至可以追溯到它叫作Acegi Security的时代),它的Web安全模型就是基于Servlet Filter构建的。毕竟,这样做是有道理的。如果我们希望拦截基于Servlet技术的Web框架的请求,以确保该请求得到了恰当的授权,那么Servlet Filter是显而易见的可行方案。但是,Spring WebFlux并不适用于这种方式。

使用Spring WebFlux编写Web应用时,我们甚至不能保证会用到Servlet。实际上,反应式Web应用很有可能构建在Netty或其他非Servlet容器上。这是否意味着基于Servlet Filter的Spring Security不能用来保护我们的Spring WebFlux应用了呢?

对于保护Spring WebFlux应用,Servlet Filter确实不是可行方案。但是,Spring Security依然可以胜任这项任务。从5.0.0版本开始,Spring Security就既能保护基于Servlet的Spring MVC,又能保护反应式的Spring WebFlux应用了。它使用了Spring的WebFilter实现这一目的,这是Spring模仿Servlet Filter的类似方案,但不依赖于Servlet API。

然而,更值得注意的是,反应式Spring Security的配置模型与我们在第4章中看到的没有太大不同。事实上,应用对Spring WebFlux与对Spring MVC的依赖是完全独立的,但Spring Security是作为同一个Spring Boot security starter提供的,也就是说,不管我们想要使用它来保护Spring MVC Web应用,还是保护使用Spring WebFlux编写的应用,都需要添加这项依赖,security starter如下所示:

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

也就是说,Spring Security的反应式和非反应式配置模型仅有几项很小的差异。我们快速对比一下这两种配置模型。

配置反应式Web应用的安全性

回忆一下,配置Spring Security来保护Spring MVC Web应用通常需要创建一个扩展自WebSecurityConfigurerAdapter的新配置类,并使用@EnableWebSecurity注解。这样的配置类将重写configuration()方法,以指定Web安全的细节,例如特定的请求路径需要哪些权限。下面这个简单的Spring Security配置类可以帮助我们回忆如何为非反应式Spring MVC应用进行安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/api/tacos", "/orders").hasAuthority("USER")
        .antMatchers("/**").permitAll();
  }
}

现在,我们看一下相同的配置如何用到反应式Spring WebFlux应用中。程序清单12.2展现了一个反应式安全配置类,它的功能与前文的安全配置大致相同。

程序清单12.2 为Spring WebFlux配置Spring Security
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

  @Bean
  public SecurityWebFilterChain securityWebFilterChain(
                                           ServerHttpSecurity http) {
    return http
        .authorizeExchange()
          .pathMatchers("/api/tacos", "/orders").hasAuthority("USER")
          .anyExchange().permitAll()
        .and()
          .build();
  }

}

可以看到,它们有很多类似的地方,同时也有所差异。这个新的配置类没有使用@EnableWebSecurity注解,而使用了@EnableWebFluxSecurity。除此之外,配置类没有扩展WebSecurityConfigurerAdapter或其他的基类,因此也就没有必要重写configure()方法。

为了取代configure()的功能,我们通过securityWebFilterChain()方法声明了一个SecurityWebFilterChain类型的bean。securityWebFilterChain()与前面配置的configure()没有太大的差异,但是也有微小的修改。

最重要的是,配置是通过给定的ServerHttpSecurity对象声明的,而不是通过HttpSecurity对象。借助ServerHttpSecurity,我们可以调用authorizeExchange(),它大致等价于authorizeRequests(),都是用来声明请求级的安全性的。

注意:ServerHttpSecurity是Spring Security 5新引入的,在反应式编程中,它模拟了HttpSecurity的功能。

在映射路径的时候,我们依然可以使用Ant风格的通配符路径,但是这里要使用pathMatchers(),而不是antMatchers()。这样做的结果就是,我们不再需要声明Ant风格的路径 “/**” 来捕获所有请求,因为anyExchange()会映射所有路径。

最后,因为我们将SecurityWebFilterChain声明为一个bean,而不是重写框架方法,所以我们需要调用build()方法将所有的安全规则聚合到一个要返回的SecurityWebFilterChain对象中。

除了这些微小的差异外,配置Spring WebFlux和Spring MVC的Web安全性并没有太多不同。那么如何获取用户的详情信息呢?

配置反应式的用户详情服务

在扩展WebSecurityConfigurerAdapter的时候,我们会重写一个configure()方法以声明安全规则,也会重写另一个configure()方法来配置认证逻辑,这通常需要我们定义一个UserDetails对象。为了帮助你回忆,如下的代码重写了configure()方法,并且在UserDetailsService的匿名实现中,使用了注入的UserRepository对象以提供根据用户名查找用户的功能:

@Autowired
UserRepository userRepo;

@Override
protected void
    configure(AuthenticationManagerBuilder auth)
    throws Exception {
  auth
    .userDetailsService(new UserDetailsService() {
      @Override
      public UserDetails loadUserByUsername(String username)
                                  throws UsernameNotFoundException {
        User user = userRepo.findByUsername(username)
        if (user == null) {
          throw new UsernameNotFoundException(
                        username " + not found")
        }
        return user.toUserDetails();
      }
    });
}

在这个非反应式的配置中,我们重写了UserDetailsService要求的唯一方法,也就是loadUserByUsername()。在这个方法内部,我们使用给定的UserRepository,实现根据用户名查找用户的功能。如果没有找到具有该用户名的用户,就会抛出UsernameNotFound Exception;如果能够找到,就调用一个辅助方法toUserDetails(),返回最终的UserDetails对象。

在反应式的安全配置中,我们不再重写configure()方法,而是声明一个ReactiveUserDetailsService bean。ReactiveUserDetailsService是UserDetailsService的反应式等价形式。与UserDetailsService类似,ReactiveUserDetailsService只需要实现一个方法。具体来讲,就是一个返回Mono<UserDetails>的findByUsername()方法,该方法返回的不再是UserDetails对象。

在下面的样例中,按照声明,ReactiveUserDetailsService bean会使用一个给定的UserRepository,我们假设它是一个反应式的Spring Data存储库(在第13章会详细讨论):

@Bean
public ReactiveUserDetailsService userDetailsService(
                                          UserRepository userRepo) {
  return new ReactiveUserDetailsService() {
    @Override
    public Mono<UserDetails> findByUsername(String username) {
      return userRepo.findByUsername(username)
        .map(user -> {
          return user.toUserDetails();
        });
    }
  };
}

在这里,我们需要返回一个Mono<UserDetails>,但是UserRepository.findByUsername()方法返回的是Mono<User>。这是一个Mono,所以可以对它进行链式操作,比如Mono<User>映射为Mono<UserDetails>的map()操作。

在本例中,map()操作使用了一个lambda表达式调用Mono所发布的User对象上的toUserDetails()方法。这个方法会将User转换为UserDetails。这样一来,“.map()”操作会返回一个Mono<UserDetails>,恰好是ReactiveUserDetailsService.findByUsername()方法所需要的。如果findByUsername()不能找到匹配的用户,那么返回的Mono就会是空的,表示由于没有匹配的用户而认证失败。

小结

  • Spring WebFlux提供了一个反应式的Web框架,它的编程模型是与Spring MVC对应的。二者甚至共享了很多相同的注解。

  • Spring 还提供了函数式编程模型,作为Spring WebFlux基于注解编程的替代方案。

  • 反应式控制器可以使用WebTestClient进行测试。

  • 在客户端,Spring 提供了WebClient,也就是Spring RestTemplate的反应式等价实现。

  • 在保护Web应用方面,尽管WebFlux在底层有一些区别,但是Spring Security 5为反应式安全所提供的编程模型与非反应式Spring MVC应用并没有很大差异。