基于Spring Security 6的OAuth2 系列之二十四 -响应式编程之一
之所以想写这一系列,是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0。无论是Spring Security的风格和以及OAuth2都做了较大改动,里面甚至将授权服务器模块都移除了,导致在配置同样功能时,花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。
注意:由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,本系列OAuth2的代码采用Spring Security6.3.0框架,所有代码都在oauth2-study项目上:https://github.com/forever1986/oauth2-study.git
目录
- 1 响应式编程
- 1.1 什么是响应式编程
- 1.2 核心概念
- 1.3 为什么要有响应式编程
- 1.4 java中常见的响应式编程框架
- 2 Spring Security的响应式编程
- 2.1 一个简单的例子
- 2.2 有何不一样
- 2.3 Spring Security登录原理
这一章我们来讲一下响应式编程,这个跟Spring Security或者OAuth2有什么关系吗?没什么关系,但是如果你细心的话,在看Spring Security 6实现的OAuth2源码时,你会发现还有一套Filter,前面都是加了Web,跟我们之前的Spring Security传统函数式编程的Filter也有些对应(当然不完全对应)
其实这是就是Spring Security使用响应式编程方式实现了一遍。那么响应式编程是什么?接下来2章,我们就分别讲一下响应式编程,Spring Security通过响应式编程实现的方式,以及最后OAuth2通过响应式编程实现的方式。
提示:如果你对响应式编程不感兴趣的话,可以之间跳过这两个系列章节。
1 响应式编程
1.1 什么是响应式编程
响应式编程是一种编程范式,它允许程序组件以声明式的方式响应数据的变化。这种编程方式使得开发者能够更容易地构建复杂的异步数据流和事件驱动的应用程序。可能这么说对于理解这个很难,下面我通过一个示例,让你感受一下什么是响应式编程。
如果你使用过java的Lambda编程方式,对于理解响应式编程就更容易一点。下面2段代码实现一样的效果,一个是使用java的Lambda表达式,一个是使用Project Reactor响应式编程框架,你先体会一下其编程风格,其实和java的Lambda表达式一样
public static void main(String[] args) {
// java本身的Lambda写法
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
Stream stream = list.stream()
.map(str -> str.substring(0,1))
.filter(str -> {
System.out.println("filter1");
return str.startsWith("a");
});
System.out.println(stream.count());
// 响应式编程Project Reactor
Flux<String> flux = Flux.just("aaa", "bbb");
flux.map(str -> str.substring(0,1))
.filter(str -> {
System.out.println("filter2");
return str.startsWith("a");
}).count().subscribe(System.out::println);
}
在上面的代码中,stream只要不调用count方法,其实什么的map、filter都不会被执行;同样的,如果flux不调用subscribe方法,其中操作也都不会被执行。也就是subscribe等同于终止操作,一旦有终止操作,中间步骤都会被执行。
1.2 核心概念
对于响应式编程,可以先了解一下观察者模式或者发布订阅模式。对于他们来说,有三个概念很重要,分别是:
数据流:数据流是响应式编程中的核心概念,它表示数据随时间的变化。就是上面例子中的Flux。
观察者:观察者是订阅数据流的对象,当数据流中的数据发生变化时,观察者会收到通知。就是上面例子中的System.out::println。
操作符:操作符是处理数据流的工具,可以对数据流进行过滤、映射、组合等操作。就是上面例子中的map、filter、count操作。
1.3 为什么要有响应式编程
这个要从同步阻塞和异步非阻塞说起。这里涉及到同步/异步,阻塞/非阻塞,拿在I/O读取数据来举例子,由于我们从磁盘读取一个文件的时间会比较长,因此我们的进程一直在等待系统内核给我们读取文件,那么就是阻塞,反之则为非阻塞。读取文件时,我们进程干其他事情,等待系统内核读取完之后通知我们,我们再去把文件拷贝过来,这就是异步,反之则为同步。
现在我们很多服务器比如Netty、Undertow 都实现了异步非阻塞的方式,这样可以提高资源利用率和吞吐量。因此响应式编程就是为了适应性这种异步非阻塞的编程方式,记住2点:
- 第一:响应式编程只是一种编程方式,并不是说函数式编程就不能实现。
- 第二:异步非阻塞是提高资源利用率和吞吐量,并不能提高某一个任务的效率。
也就是说响应式编程只是一种编程风格,这种风格时候那种并发式场景的编程。因为它抽离了代码与处理器之间的耦合,程序员只需要关注你要处理数据的方法即可,然后将其传入到下游。
1.4 java中常见的响应式编程框架
我们现在知道响应式编程只是一种适合并发场景的编程风格,对于其编程的风格或者规范,可以参考《Reactive Stream》文档。在java中,有几个已经实现的框架可以使用:
- RxJava :由 Netflix 开发的响应式扩展库,早于 Reactor,提供了强大的数据流处理能力。https://github.com/ReactiveX/RxJava
- Project Reactor:Project Reactor是Spring Framework的一部分,提供了基于Java 8的响应式编程库。它使用Publisher和Subscriber模式,支持多种响应式类型。https://projectreactor.io/
- Vert.x:Vert.x是一个用于构建响应式应用程序的工具包,它提供了事件驱动的非阻塞I/O模型。Vert.x的核心是Verticles,它们是轻量级的、独立的、可伸缩的单元。https://vertx.io/
- Akka:Akka是一个用于构建高度并发、分布式和容错性的应用程序的工具和运行时。它提供了一个统一的编程模型来处理并发和分布式系统中的复杂性。https://akka.io/
2 Spring Security的响应式编程
这里先了解一个Spring Security里面的响应式编程,因为我们的OAuth2是基于Spring Security实现的,只有了解Spring Security如何使用响应式编程框架,才能更好了解OAuth2使用响应式编程风格。
2.1 一个简单的例子
下面我们做一个简单的Spring Security的实例,看看与传统函数式编程有何不一样。
代码参考lesson18子模块
1)新建lesson18子模块,其pom如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
2)在resources目录下,建立yaml文件,制定端口
server:
port: 9000
3)在config包下面,创建Spring Security的配置SecurityConfig类
@Configuration
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception {
http
// 所有访问都必须认证
.authorizeExchange(exchange -> exchange.anyExchange().authenticated())
// 禁用csrf,因为登录和登出是post请求,csrf会屏蔽掉post请求
.csrf(ServerHttpSecurity.CsrfSpec::disable)
// 默认配置
.formLogin(Customizer.withDefaults());
return http.build();
}
/**
* 默认一个用户
*/
@Bean
public ReactiveUserDetailsService userDetailsService(){
return new ReactiveUserDetailsService(){
@Override
public Mono<UserDetails> findByUsername(String username) {
UserDetails userDetails = User.withUsername("test")
.password("{noop}1234")
.build();
return Mono.just(userDetails);
}
};
}
}
4)在controller包下,定义demo接口
@RestController
public class DemoController {
@GetMapping("/demo")
public Mono<String> demo() {
return Mono.just("Hello, Reactive");
}
}
5)定义启动类Oauth2Lesson18Application,并启动项目
@SpringBootApplication
public class Oauth2Lesson18Application {
public static void main(String[] args) {
SpringApplication.run(Oauth2Lesson18Application.class, args);
}
}
6)测试,你访问:http://localhost:9000/demo 时,会跳转到登录界面输入test和1234,就可以访问
2.2 有何不一样
1)首先,引入的pom就不一样,原先我们引入starter-web,现在引入starter-webflux,我们可以看看2者引入依赖的区别(左边是starter-webflux,右边是starter-web)
2)其次,使用的服务器不一样,starter-webflux使用的是netty,而starter-web使用的是tomcat。这是因为响应式编程就是为了异步非阻塞的方式而生,而netty是一个异步非阻塞的服务器。虽然tomcat也支持,但是在性能上,netty更佳,因此默认使用netty。
3)再次,编程风格不一样,无论是SecurityConfig还是DemoController都与原先的不一样。由于Spring Security采用底层是Spring,因此就使用Projec Reactor框架,关于Projec Reactor框架大家可以先看看,在来学习这一部分会容易些。
4)最后,底层原理实现方式不一样,可以看到其过滤器与原先的是不一样的过滤器,虽然名字很像,其实你可以理解是采用响应式编程重写了一遍。
2.3 Spring Security登录原理
那么我们来从底层分析一下Spring Security响应式编程风格实现的登录原理,如果不知道Spring Security原先使用函数式编程的底层原理,可以参考我之前写的《Spring Security系列》,这里只会说明响应式编程的改造部分。由于Spring Security是基于Spring,而Spring的响应式编程又是基于Project Reactor,因此在看这一部分之前,最好还是先了解一下Project Reactor,因为Spring Security原理还是一样的,只是实现风格不同,你不了解Project Reactor会看不懂其代码。
1)先通过下面的流程图,大概回顾一下Spring Security的认证底层流程,其中几个关键的内容,一个是AuthenticationManager进行认证,最终使用AuthenticationProvider进行认证。
2)接下来,我们来看看Spring Security改成响应式编程的方式(这里需要明确一点,就是对于Spring Security的认证原理是没有变化,只不过换了一种方式)。
3)我们知道Spring Security还是基于Filter过滤器,那么我们看看新的认证过滤器:AuthenticationWebFilter
4)上图中AuthenticationWebFilter的第①步,使用ServerWebExchangeMatcher进行url验证
5)上图中AuthenticationWebFilter的第②步,使用ServerAuthenticationConverter转换前端传过来的参数username和password,最终是使用ServerFormLoginAuthenticationConverter转换(注意这里可以是其它Converter,默认是表单传输)。
6)AuthenticationWebFilter的第③步,使用ReactiveAuthenticationManager进行认证,其实是调用UserDetailsRepositoryReactiveAuthenticationManager进行认证,而UserDetailsRepositoryReactiveAuthenticationManager继承AbstractUserDetailsReactiveAuthenticationManager。我们先看UserDetailsRepositoryReactiveAuthenticationManager的内容:
7)我们再来看看AbstractUserDetailsReactiveAuthenticationManager的authenticate认证方法:
8)从上面分析之后,我们可以大概画出流程图如下:
总结:从流程上来看,基本上和原先函数式编程的原理是一致,只不过使用了响应式编程实现一遍
结语:通过本章,我们了解了响应式编程以及Spring Security的响应式编程原理,这是理解一个基础。Spring Security使用响应式编程实现OAuth2的Client和Resource2个功能,由于授权服务器独立出去,目前版本还未实现响应式编程方式,不过下一章我们还是会了解如何搭建响应式编程的OAuth2的资源服务器。