11.5 保护反应式Web API

11.5 保护反应式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模仿ServletFilter的类似方案,但是它不依赖于Servlet API。

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

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

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

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

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

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders").hasAuthority("USER")
.antMatchers("/**").permitAll();
}
}

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

程序清单11.2 为Spring WebFlux配置Spring Security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/design", "/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安全性并没有太多不同。那么如何获取用户的详情信息呢?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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,实现根据用户名来查找用户的功能。如果没有找到该名称的用户,就会抛出UsernameNotFoundException。如果能够找到,就调用一个辅助方法toUserDetails(),返回最终的UserDetails对象。

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

在下面的样例中,ReactiveUserDetailsService bean会给定一个UserRepository,我们假设它是一个反应式的Spring Data repository(在第12章我们将会详细讨论):

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
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,所以可以对它进行链式操作,比如进行map()操作,将Mono<User>映射为Mono<UserDetails>

在本例中,map()操作使用了一个lambda表达式,它调用了Mono所发布的User对象上的toUserDetails()方法。这个方法会将User转换为UserDetails。这样的话,“.map()”操作会返回一个Mono<UserDetails>,恰好就是ReactiveUserDetailsService.findByUsername()方法所需要的。