摘要
在微服务架构体系里,API网关作为整个系统的统一入口,肩负着路由转发、鉴权认证、流量控制等诸多关键职责。本文详细记录了在Spring Cloud Gateway(基于WebFlux)中集成若依框架(基于MVC)鉴权功能时所经历的技术选型过程,深入剖析了WebFlux与MVC的技术差异,并阐释了最终选择HTTP远程鉴权方案的决策依据。
目录
- 背景
- [1.1 Gateway需要集成若依鉴权](#1.1 Gateway需要集成若依鉴权)
- [1.2 Gateway基于WebFlux](#1.2 Gateway基于WebFlux)
- [1.3 若依框架基于MVC](#1.3 若依框架基于MVC)
- [WebFlux vs MVC对比](#WebFlux vs MVC对比)
- [2.1 编程模型差异](#2.1 编程模型差异)
- [2.2 线程模型差异](#2.2 线程模型差异)
- [2.3 性能特性差异](#2.3 性能特性差异)
- [2.4 生态成熟度对比](#2.4 生态成熟度对比)
- 技术决策分析
- [3.1 方案一:改造若依支持WebFlux](#3.1 方案一:改造若依支持WebFlux)
- [3.2 方案二:本地鉴权](#3.2 方案二:本地鉴权)
- [3.3 方案三:远程鉴权中心](#3.3 方案三:远程鉴权中心)
- [3.4 各方案优劣对比](#3.4 各方案优劣对比)
- 放弃WebFlux改造的原因
- [4.1 项目其他模块都是MVC](#4.1 项目其他模块都是MVC)
- [4.2 WebFlux生态不熟悉](#4.2 WebFlux生态不熟悉)
- [4.3 异步结构Debug困难](#4.3 异步结构Debug困难)
- [4.4 可能引入雪崩风险](#4.4 可能引入雪崩风险)
- 最终选型:远程鉴权中心
- [5.1 HTTP方案(选择)](#5.1 HTTP方案(选择))
- [5.2 gRPC方案(备选但放弃)](#5.2 gRPC方案(备选但放弃))
- [5.3 选择理由](#5.3 选择理由)
- 双环境适配实现
- [6.1 Gateway WebFlux环境](#6.1 Gateway WebFlux环境)
- [6.2 其他服务MVC环境](#6.2 其他服务MVC环境)
- [6.3 统一抽象层设计](#6.3 统一抽象层设计)
- 经验总结
- [7.1 技术选型考虑因素](#7.1 技术选型考虑因素)
- [7.2 WebFlux适用场景](#7.2 WebFlux适用场景)
- [7.3 项目一致性重要性](#7.3 项目一致性重要性)
一、背景
1.1 Gateway需要集成若依鉴权
在合同审查平台的架构设计工作中,我们选定Spring Cloud Gateway作为API网关,以此来统一管理所有微服务的入口。同时,引入若依框架作为认证授权的基础架构,目的是让Gateway能够复用若依的权限体系。这就如同在一个大型商场中,Gateway是所有用户进入各个店铺(微服务)的大门,而若依框架则是负责检查用户是否有资格进入特定店铺(访问特定权限资源)的安保系统。
1.2 Gateway基于WebFlux
这实际上是一种"被动选择"。并非我们主动挑选WebFlux,而是因为Spring Cloud Gateway的路由功能自身便是基于WebFlux实现的。
Spring Cloud Gateway作为Spring官方力荐的下一代API网关(用以替代第一代的Zuul),其底层架构完全构建于Spring WebFlux之上。这就意味着:
- 若要运用Spring Cloud Gateway强大的路由功能,就不得不接纳WebFlux。
- 无法在Gateway中使用传统的Spring MVC组件。
- 这是技术框架所施加的强制性约束,并非我们自主的技术选型决策。
查看contract - gateway的pom.xml文件,能清晰看到WebFlux的依赖配置:
xml
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<!-- Spring Boot WebFlux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
上述代码中,通过spring - cloud - starter - gateway - server - webflux和spring - boot - starter - webflux这两个依赖,引入了Spring Cloud Gateway和Spring Boot WebFlux相关的功能,表明该项目基于WebFlux构建。
1.3 若依框架基于MVC
若依框架作为一款成熟的开源权限管理系统,其核心是搭建在传统的Spring MVC之上,借助Servlet容器来处理请求。查看RuoYi - Vue的pom.xml可以发现:
xml
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.5.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
这段配置体现了若依框架对Spring Boot相关依赖的引入,基于此构建其基于Spring MVC的功能体系,也由此造成了与WebFlux在编程模型上的本质区别。
二、WebFlux vs MVC对比
2.1 编程模型差异
| 维度 | Spring MVC | Spring WebFlux |
|---|---|---|
| 编程范式 | 命令式编程 | 响应式编程 |
| 返回类型 | 直接返回对象 | Mono/Flux |
| 阻塞特性 | 阻塞I/O | 非阻塞I/O |
| 代码风格 | 同步调用 | 链式调用 + Lambda |
| 学习曲线 | 平缓 | 较陡 |
例如,对于同样获取用户信息并关联角色的业务逻辑,两种框架的写法差异显著:
java
// MVC风格(阻塞)
public User getUserById(Long id) {
User user = userRepository.findById(id);
List<Role> roles = roleRepository.findByUserId(id);
user.setRoles(roles);
return user;
}
// WebFlux风格(非阻塞)
public Mono<User> getUserById(Long id) {
return userRepository.findById(id)
.flatMap(user -> roleRepository.findByUserId(user.getId())
.collectList()
.doOnNext(user::setRoles)
.thenReturn(user));
}
在上述代码示例中,MVC风格采用传统的同步方式,按照顺序依次执行数据库查询和对象赋值操作。而WebFlux风格则运用响应式编程,通过Mono和链式调用,以非阻塞的方式处理异步操作。这就好比MVC是按照顺序一项一项完成任务,而WebFlux则可以同时处理多个任务,并在合适的时机组合结果。
2.2 线程模型差异
Spring MVC采用传统的Thread - per - Request模型,即每个请求都会占用一个独立线程,直至响应完成。而WebFlux则利用少量固定线程来应对大量并发请求:
MVC线程模型:
请求1 → 线程1 → [阻塞等待DB] → 返回
请求2 → 线程2 → [阻塞等待DB] → 返回
请求3 → 线程3 → [阻塞等待DB] → 返回
WebFlux线程模型:
请求1 ┐
请求2 ├→ 少量事件循环线程 → 非阻塞I/O ─→ 返回
请求3 ┘
可以将MVC线程模型想象成每个用户(请求)都有一个专属的服务员(线程)全程陪同,在等待上菜(数据库查询等操作)时,服务员只能干等。而WebFlux的线程模型则像是几个服务员同时服务众多用户,在等待上菜时,服务员可以去处理其他用户的需求。
2.3 性能特性差异
WebFlux在以下场景中优势凸显:
- 高并发I/O密集型操作,例如在一个大型文件下载网站,同时有大量用户请求下载文件,WebFlux能够高效处理这些I/O操作。
- 需要处理大量长连接,如实时聊天应用,众多用户保持长连接与服务器通信,WebFlux可以更好地管理这些连接。
- 流式数据处理,比如实时数据监控系统,持续接收并处理数据流。
然而,在若依鉴权这类场景下,其优势并不突出。因为权限验证主要依赖数据库查询,依然是I/O等待过程,就如同在一个用户数量相对稳定的小餐馆,即使采用了大型餐厅的高效服务模式(WebFlux),也难以体现出明显优势。
2.4 生态成熟度对比
| 方面 | Spring MVC | Spring WebFlux |
|---|---|---|
| 历史 | 2004年 | 2017年 |
| 社区成熟度 | 非常成熟 | 持续发展 |
| 第三方库兼容 | 广泛兼容 | 部分库不支持 |
| 调试便利性 | 堆栈清晰 | 异步堆栈复杂 |
| 资料丰富度 | 极其丰富 | 相对较少 |
Spring MVC由于诞生时间较早,经过多年发展,积累了丰富的社区资源、大量兼容的第三方库以及清晰的调试方法,就如同是一座发展成熟、设施完备的城市。而Spring WebFlux作为较新的技术,虽然在不断发展,但在这些方面相对较弱,类似于正在建设中的新兴城市。
三、技术决策分析
3.1 方案一:改造若依支持WebFlux
思路:将若依框架的核心模块全面改造为WebFlux响应式编程模型。
优势:
- 技术栈实现统一,全部采用响应式编程,使得整个系统在编程风格和技术体系上更加一致,就像一个团队都使用同一种语言交流,沟通更顺畅。
- 理论上性能最优,能够充分发挥WebFlux的优势,应对高并发场景。
劣势:
- 改造工作量巨大,需要对核心逻辑进行重写,这就好比要把一座已经建好的大楼推倒重新按照新的设计方案建造。
- MyBatis等组件需要迁移到R2DBC,涉及到数据库访问方式的重大改变。
- 若依框架生态丰富,改造后可能无法复用社区资源,就像原本有很多好用的工具,改造后却不能用了。
3.2 方案二:本地鉴权
思路:在Gateway内部直接实现权限验证逻辑,独立维护一套用户权限数据。
优势:
- 避免远程调用,性能最好,减少了网络通信带来的开销,就像在自己家里就能完成所有事情,不需要跑到外面去。
- 架构简单,不依赖外部服务,降低了系统的复杂性。
劣势:
- 数据冗余,需要同步用户权限数据,这就好比一份文件在多个地方都要保存一份,增加了管理成本。
- 两套代码维护成本高,不仅要维护Gateway中的权限验证代码,还要维护若依框架原有的相关代码。
- 若依框架的功能无法复用,浪费了若依框架已有的成熟功能。
3.3 方案三:远程鉴权中心
思路:保持若依框架不变,Gateway通过Feign客户端调用若依的鉴权接口。
优势:
- 若依框架无需改动,最大程度地保留了其原有功能和生态,就像一辆运行良好的汽车,不需要对其进行大修。
- 复用成熟的权限功能,利用若依框架已经完善的权限管理能力。
- 数据来源唯一,避免了数据不一致的问题,所有权限验证都基于若依框架中的数据。
劣势:
- 增加网络调用开销,每次权限验证都需要通过网络请求若依框架的鉴权接口,就像每次做事都要跑到别的地方去问一下。
- 需要处理服务降级,以防若依鉴权服务出现故障时,系统仍能保持一定的可用性。
3.4 各方案优劣对比
| 评估维度 | 方案一:改造若依 | 方案二:本地鉴权 | 方案三:远程鉴权 |
|---|---|---|---|
| 开发成本 | 极高 | 中等 | 低 |
| 维护成本 | 低(若统一) | 高(双维护) | 低 |
| 性能 | 最优 | 最优 | 略有损耗 |
| 数据一致性 | 强一致 | 需要同步 | 强一致 |
| 技术风险 | 高 | 中 | 低 |
| 复用性 | - | 差 | 好 |
四、放弃WebFlux改造的原因
4.1 项目其他模块都是MVC
查看项目结构,会发现除了Gateway采用WebFlux外,其他核心服务如contract - management、contract - review - engine等均使用传统的Spring MVC:
xml
<!-- contract - gateway采用WebFlux -->
<dependency>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 其他服务采用MVC -->
<dependency>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果仅仅为了Gateway而改造若依,会致使项目技术栈分裂更为严重,就像一个团队里,大部分人说一种语言,只有少数人说另一种语言,交流和协作会变得困难。
4.2 WebFlux生态不熟悉
团队对Spring MVC拥有丰富的开发经验,但对响应式编程相对陌生。这意味着学习成本和调试难度都会增加,就像要去一个陌生的地方,不仅要学习当地的语言和文化,遇到问题时解决起来也更困难。
4.3 异步结构Debug困难
WebFlux的响应式编程堆栈复杂,当出现问题时,定位和排查的难度远高于同步代码。这就好比在一个迷宫中寻找出路,同步代码的迷宫可能路线比较清晰,而异步结构的迷宫则充满了错综复杂的路径,更难找到问题所在。
4.4 可能引入雪崩风险
将成熟的若依框架改造为WebFlux,可能会引入未知的问题和风险,进而影响系统的稳定性。就像对一座原本坚固的桥梁进行大规模改造,可能会破坏其原有的稳定性,带来意想不到的危险。
五、最终选型:远程鉴权中心
5.1 HTTP方案(选择)
最终确定通过HTTP协议调用若依鉴权服务的方案,使用Feign客户端进行远程调用。
核心配置如下:
java
@FeignClient(name = "contract - security - ruoyi",
contextId = "remoteAuthFeignService",
path = "/contract - security - ruoyi/")
public interface RemoteAuthFeignService {
/**
* 远程权限验证
*/
@PostMapping("/remote/auth/validate")
AjaxResult validatePermission(@RequestBody AuthValidateRequest request);
}
上述代码定义了一个Feign客户端接口RemoteAuthFeignService,通过@FeignClient注解指定了服务名称、上下文ID和路径。validatePermission方法用于远程调用若依鉴权服务的权限验证接口。
5.2 gRPC方案(备选但放弃)
虽然gRPC在性能上优于HTTP,但考虑到:
- 若依框架没有gRPC支持,需要额外开发适配功能。
- 团队对gRPC不熟悉,增加了开发和维护的难度。
- 调试工具相对缺乏,不利于问题排查和解决。
最终没有选择gRPC方案。这就好比有一辆速度更快的车(gRPC),但需要特定的道路(若依框架支持),且司机(团队)不熟悉驾驶方法,维修工具(调试工具)也不好找,所以最终选择了更熟悉的交通工具(HTTP)。
5.3 选择理由
- 技术栈复用:Feign客户端是Spring Cloud生态的标准组件,团队熟悉,能够充分利用已有的技术知识,减少学习成本。
- 开发效率高:只需定义接口,无需处理底层通信,就像只需要告诉别人你要什么,而不需要关心怎么去拿,大大提高了开发速度。
- 调试便利:可以使用常规的HTTP工具进行调试,降低了调试门槛,更容易发现和解决问题。
- 渐进式演进:后续如果需要优化性能,可以逐步调整,为系统的发展提供了灵活性。
六、双环境适配实现
6.1 Gateway WebFlux环境
针对WebFlux环境,ruoyi - feign模块提供了RemoteAuthWebFilter:
java
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RemoteAuthWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 1. 获取HandlerMethod
Method handlerMethod = WebFluxRouteUtil.getHandlerMethod(exchange);
// 2. 检查注解
RemotePreAuthorize annotation = getRemotePreAuthorizeAnnotation(handlerMethod);
// 3. 提取token
String token = extractToken(exchange.getRequest());
// 4. 异步验证权限
return validateRemotePermission(token, annotation.value())
.flatMap(has