Spring Cloud
本文最后更新于 2023-12-09,文章内容可能已经过时。
Spring Cloud
1.介绍:
Spring Cloud 是一个用于构建分布式系统和微服务架构的开源框架。它提供了一系列的工具和组件,用于解决分布式系统中的一些常见问题,例如服务发现、配置管理、负载均衡、断路器模式等。Spring Cloud 基于 Spring Boot 构建,通过简化开发流程,帮助开发者更容易地构建和管理分布式系统。
1.1 Spring Cloud与Spring Boot:
Spring Cloud 和 Spring Boot 是 Spring Framework 生态系统中的两个关键项目,它们有不同的关注点和目标,但通常一起使用以构建分布式系统。
Spring Boot:
Spring Boot 旨在简化基于 Spring 的应用程序的开发和部署。它提供了一种约定大于配置的方式,通过默认设置和自动配置来减少开发者的工作量。
Spring Boot 使得创建独立的、可运行的、生产级别的 Spring 应用变得更加容易。它包括一组预定义的依赖项和插件,可以通过简单的配置来使用。
Spring Boot 的目标是减少开发者的配置工作,使他们更专注于业务逻辑而不是底层的框架配置。
Spring Cloud:
Spring Cloud 用于构建分布式系统的解决方案,特别是微服务架构。它提供了一组工具和组件,用于处理微服务体系结构中的常见问题,例如服务发现、配置管理、负载均衡、断路器模式等。
Spring Cloud 构建在 Spring Boot 之上,利用了 Spring Boot 的便利性。它扩展了 Spring Boot,提供了更多面向分布式系统的功能。
Spring Cloud 的目标是帮助开发者构建具有高可用性、可伸缩性和弹性的分布式系统。
总体而言,Spring Boot 是构建独立应用的工具,而 Spring Cloud 是构建分布式系统和微服务架构的工具。Spring Cloud 使用 Spring Boot 来简化微服务的开发和部署,因此在 Spring Cloud 项目中通常会使用 Spring Boot。因此,可以说 Spring Cloud 包含了 Spring Boot,并在此基础上提供了更多的分布式系统相关的功能。
在 Spring Cloud 中,每个独立的微服务通常都是一个独立的 Spring Boot 项目,它使用 Spring Boot 的简化配置和开发模型。每个微服务负责处理特定的业务功能,并与其他微服务协同工作以构建完整的分布式系统。
Spring Boot 提供了许多便捷的功能,使得开发者能够更轻松地创建独立可运行的应用程序。Spring Cloud 则在此基础上构建,为微服务架构提供了一系列的解决方案,如服务发现、配置管理、负载均衡、断路器模式等,以支持构建具有弹性和高可用性的分布式系统。
因此,每个微服务可以被视为一个 Spring Boot 项目,但在 Spring Cloud 的背景下,它还会集成 Spring Cloud 提供的一些组件,以实现分布式系统的各种功能。这种组合的方式使得开发者能够更容易地构建和管理复杂的微服务体系结构。
简单来说,spring cloud就是一系列的spring boot项目加上相关的分布式组件.
1.2 Spring Cloud 与Spring Cloud Alibaba:
Spring Cloud 和 Spring Cloud Alibaba 都是用于构建分布式系统和微服务架构的框架,它们在 Spring 生态系统的基础上提供了一系列的工具和组件。然而,Spring Cloud Alibaba 是在 Spring Cloud 的基础上进行扩展和增强,以满足更多与阿里巴巴生态系统相关的需求。
以下是它们之间的一些主要区别和联系:
Spring Cloud:
Spring Cloud 是由 Pivotal 团队开发和维护的一套构建分布式系统的工具。
提供了一系列的组件,如服务发现、配置管理、负载均衡、断路器模式、API 网关等。
广泛支持多种云平台,如AWS、Azure、Google Cloud等。
不与特定的云平台或厂商绑定。
Spring Cloud Alibaba:
Spring Cloud Alibaba 是由阿里巴巴团队开发和维护的一套增强 Spring Cloud 的工具,以更好地集成阿里云服务和阿里巴巴的中间件。
强调与阿里云相关的功能,如分布式事务、消息队列、配置中心等。
提供了一些新的组件,如 Nacos(服务注册与发现)、Sentinel(流量控制和熔断降级)、RocketMQ(消息队列)等。
更密切地与阿里巴巴的中间件和服务集成,使得在阿里云上构建微服务更加方便。
简单来说,他俩都是基于spring boot 设计的,区别就是使用的中间件不同.
2.使用:
综上所述,学习spring cloud,就是学习一系列的分布式组件.
2.1 Nacos:
微服务所面临的第一个问题是,如何在各个服务之间互通,即怎么在服务a中调用服务b的接口。要实现这一功能,首先需要的就是服务发现。
Nacos(中文名“云注册中心与配置中心”)是一个开源的分布式系统服务基础设施,用于实现动态服务发现、服务配置和服务元数据。Nacos最初由阿里巴巴集团开发,后来成为了Apache软件基金会的一个孵化项目。
主要有以下功能:
1)服务发现: 支持 DNS 与 RPC 服务发现,也提供原生 SDK 、OpenAPI 等多种服务注册方式和 DNS、HTTP 与 API 等多种服务发现方式。 2)服务健康监测: Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。 3)动态配置服务: Nacos 提供配置统一管理功能,能够帮助我们将配置以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。 4)动态 DNS 服务: Nacos 支持动态 DNS 服务权重路由,能够让我们很容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单 DNS 解析服务。 5)服务及其元数据管理: Nacos 支持从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。
2.1.1 安装:
//拉取镜像
docker pull nacos/nacos-server
//挂载目录
mkdir -p /mydata/nacos/logs/ #新建logs目录
mkdir -p /mydata/nacos/init.d/
//获取数据库支持
mysql新建nacos-config的数据库,并执行脚本 sql脚本地址如下:
https://github.com/alibaba/nacos/blob/master/config/src/main/resources/META-INF/nacos-db.sql
//修改配置文件
server.contextPath=/nacos
server.servlet.contextPath=/nacos
server.port=8848
spring.datasource.platform=mysql
#配置持久化数据库相关信息 ####################################################
db.num=1
db.url.0=jdbc:mysql://xx.xx.xx.x:3306/nacos-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=root
##########################################################################
nacos.cmdb.dumpTaskInterval=3600
nacos.cmdb.eventTaskInterval=10
nacos.cmdb.labelTaskInterval=300
nacos.cmdb.loadDataAtStart=false
management.metrics.export.elastic.enabled=false
management.metrics.export.influx.enabled=false
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i
nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/**
nacos.naming.distro.taskDispatchThreadCount=1
nacos.naming.distro.taskDispatchPeriod=200
nacos.naming.distro.batchSyncKeyCount=1000
nacos.naming.distro.initDataRatio=0.9
nacos.naming.distro.syncRetryDelay=5000
nacos.naming.data.warmup=true
nacos.naming.expireInstance=true
//启动容器:
docker run -d -p 8849:8848 --name nacos_8849 \
--privileged=true \
--restart=always \
-e JVM_XMS=256m \
-e JVM_XMX=256m \
-e MODE=standalone \
-e PREFER_HOST_MODE=hostname \
-v /mydata/nacos/logs:/home/nacos/logs \
-v /mydata/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties \
--restart=always \
nacos/nacos-server
启动成功后.访问http://自己的ip:8849/nacos/即可进入控制台(记得云服务器的话打开防火墙)
默认用户名:nacos,默认密码:nacos
2.1.2 集成:
2.1.2.1 依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring‐cloud‐starter‐alibaba‐nacos‐discovery</artifactId>
</dependency>
2.1.2.2 配置:
spring:
application:
name: 服务名称
cloud:
nacos:
discovery:
server‐addr: 127.0.0.1:8848
注意,上面这个地址不是服务的地址,而是nacos的地址。
在服务启动的时候,它会根据上面这个地址去请求nacos,并且把自己的ip,端口,服务名之类的东西传上去。
如果配置127.0.0.1,那么说明nacos和这个服务在同一台机器上。
2.1.2.3 调用:
此处为直接调用方法.
import com.alibaba.nacos.api.naming.NamingFactory;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import java.util.List;
public class ServiceDiscoveryExample {
public static void main(String[] args) throws Exception {
// Nacos 服务器地址
String serverAddr = "localhost:8848";
// Nacos 命名空间
String namespace = "your-namespace";
// 服务名称
String serviceName = "your-service-name";
// 创建 Nacos NamingService 对象
NamingService namingService = NamingFactory.createNamingService(serverAddr);
// 获取服务的所有实例信息
List<Instance> instances = namingService.getAllInstances(serviceName);
// 遍历实例信息
for (Instance instance : instances) {
System.out.println("Instance: " + instance);
// 这里可以获取实例的 IP 地址、端口等信息
String ip = instance.getIp();
int port = instance.getPort();
// 这里可以根据实际情况调用其他服务的接口
System.out.println("Call other service at: http://" + ip + ":" + port);
}
}
}
2.2 OpenFegin:
从上面的示例代码可以知道,nacos虽然可以帮助我们调用其他模块的接口,但是非常的复杂,那么有没有什么简单的方式可以实现这种跨模块之间的调用呢?这就是OpenFegin.
在2018年初,Spring Cloud宣布将原本属于Netflix的Feign整合到Spring Cloud中,并将其称为OpenFeign。因此,实际上,OpenFeign就是Spring Cloud对Feign的一个重新打包和整合。
OpenFeign是一个基于Java的声明式Web服务客户端库,它简化了通过HTTP请求调用远程服务的过程。OpenFeign的设计目标是使编写HTTP客户端变得更加简单,并且它提供了一种声明式的方式来定义Web服务客户端。
以下是一些OpenFeign的主要特点和用法:
声明式API: OpenFeign允许你使用注解来定义Web服务客户端的接口。这些注解包括
@FeignClient
用于标识一个接口为Feign客户端,以及@RequestMapping
等用于定义请求映射的注解。集成了Ribbon和Hystrix: OpenFeign内置了对Netflix Ribbon(负载均衡)和Netflix Hystrix(容错和断路器)的支持。这使得在使用OpenFeign时,你无需额外配置这些组件,它们会自动集成并生效。
自动编码和解码: OpenFeign自动处理HTTP请求和响应的编码和解码过程。你只需要关注业务逻辑,而不用担心底层的HTTP通信细节。
支持多种HTTP客户端: OpenFeign可以与多种HTTP客户端库集成,包括默认的
HttpURLConnection
、Apache HttpClient等。动态代理: OpenFeign通过动态代理生成实际的HTTP请求。这样,你只需要定义接口和方法,而不需要实现它们,OpenFeign会在运行时生成代理类来处理实际的HTTP请求。
支持Spring Cloud: OpenFeign是Spring Cloud生态系统的一部分,它与其他Spring Cloud组件(如Eureka服务注册与发现、Config服务配置等)集成得很好,使得在微服务架构中更容易使用。
2.2.1 使用:
首先要注意的是,fegin本身已经继承了nacos.
这里以服务a调用服务b的c方法为例子。
Fegin主要通过声明式客户端的方式调用其他服务的方法。
在Feign中,声明式客户端是指通过Java接口和注解声明的客户端,用于描述和定义远程服务的调用契约。这种声明式的方式使得开发者无需编写底层的HTTP请求和响应处理代码,而是通过定义接口的方式,描述了对远程服务的调用。
feign使用动态代理技术,在运行时自动生成实际的方法实现。具体而言,Feign会根据你定义的接口以及相关的注解,生成能够执行HTTP请求的代码。
当你定义一个带有@FeignClient
注解的接口时,Feign将会创建一个代理类,该代理类会在运行时调用远程服务。这个代理类是动态生成的,不需要你手动实现接口中的方法。Feign的动态代理机制使得开发者能够专注于接口的定义,而无需关心底层的HTTP请求和响应的处理。Feign会自动根据接口的定义生成合适的HTTP请求,并处理响应结果。
2.2.1.1 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2.1.1 编写声明式客户端:
在服务 B 中,定义一个 Feign 接口,用于描述服务 B 中的业务方法。name为服务b在上文的nacos配置中提交的服务名称。
@FeignClient(name = "service-b")
public interface ServiceBFeignClient {
@GetMapping("/api/c")
String methodC();
}
2.2.1.2 编写实现逻辑:
在服务b中编写在fegin中定义的接口的逻辑:
@Service
public class ServiceBServiceImpl implements ServiceBService {
@Autowired
private SomeRepository someRepository;
@Override
public String methodC() {
// 具体的业务逻辑,可能包含数据库查询
SomeEntity result = someRepository.findById(1L).orElse(null);
return (result != null) ? result.getValue() : "Default";
}
}
2.2.1.3 引入相关依赖:
当在a服务中调用b服务的相关逻辑的时候,要把b服务的client相关依赖引入a服务中。
2.2.1.4 使用:
@Service
public class ServiceAService {
private final ServiceBFeignClient serviceBFeignClient;
@Autowired
public ServiceAService(ServiceBFeignClient serviceBFeignClient) {
this.serviceBFeignClient = serviceBFeignClient;
}
public String callServiceBMethodC() {
// 调用服务 B 的 Feign 接口
return serviceBFeignClient.methodC();
}
}
2.2.1.5 编程式客户端:
Feign 主要通过声明式客户端的方式使用。声明式客户端的优点在于开发者只需通过接口的方式定义远程服务的调用契约,而具体的远程调用细节(如 HTTP 请求的构建、发送和响应的处理)由 Feign 框架自动处理。这种方式简化了代码,提高了开发效率。
但是,除了声明式客户端之外,Feign 还提供了一种编程式客户端的方式,允许你直接通过 Feign 的 API 进行手动构建和发送 HTTP 请求。这种方式相对于声明式客户端更加灵活,但也更加底层,需要开发者更多地处理请求和响应的细节。
以下是一个简单的编程式 Feign 客户端的示例:
import feign.Feign;
import feign.RequestLine;
import feign.Response;
public class MyFeignClient {
public static void main(String[] args) {
MyFeignClient client = Feign.builder()
.target(MyFeignClient.class, "https://api.example.com");
Response response = client.getResource();
System.out.println("Response Code: " + response.status());
System.out.println("Response Body: " + response.body().asString());
}
@RequestLine("GET /api/resource")
Response getResource();
}
在这个示例中,我们使用了 feign.Feign
类的 builder()
方法创建了一个 Feign 客户端,并通过 target
方法指定了目标服务的接口和基础 URL。然后,我们定义了一个方法 getResource
来发送 GET 请求,并通过 Response
对象处理响应的信息。这是一种更加底层、手动的方式,需要开发者处理更多的细节。
总体而言,虽然 Feign 的主要使用方式是声明式客户端,但你也可以选择使用编程式客户端,具体取决于你的需求和偏好。然而,声明式客户端通常更符合 Spring Cloud 中的微服务开发理念,提供了更高层次的抽象和更方便的开发体验。
2.2.2 实现:
OpenFeign是一个声明式的HTTP客户端,它的实现基于Java的动态代理和Spring MVC。以下是OpenFeign的基本实现原理:
接口定义: 用户定义一个接口,该接口用于描述远程服务的API。在接口方法上使用注解来指定HTTP请求的细节,如URL、HTTP方法、请求参数等。
javaCopy code@FeignClient(name = "example-service") public interface ExampleFeignClient { @GetMapping("/exampleEndpoint") String getExampleData(); }
动态代理: 当Spring启动时,OpenFeign会使用动态代理生成一个代理类,该代理类实现了用户定义的接口。代理类负责将接口方法的调用转发到实际的HTTP请求。
注解处理: OpenFeign会扫描接口上的注解,特别是
@FeignClient
注解。该注解指定了要调用的服务的名称,以及其他配置项。在方法级别,使用诸如@GetMapping
、@PostMapping
等注解来定义具体的HTTP请求。请求构建: 当调用Feign接口的方法时,代理类会根据注解和方法参数构建HTTP请求。这包括URL、HTTP方法、请求头、请求参数等。
请求发送: 构建好的HTTP请求会被发送到目标服务。OpenFeign使用底层的HTTP客户端(通常是Spring的
RestTemplate
或WebClient
)来执行实际的HTTP请求。响应处理: 接收到目标服务的响应后,代理类会将响应解析为接口方法的返回类型,并返回给调用方。
在底层,OpenFeign通常使用了动态代理、反射和Spring MVC的注解处理机制。它能够根据接口定义动态生成代码,以简化与远程服务的通信过程。在整个过程中,开发者只需要关注接口定义和注解的使用,而无需关心底层的HTTP请求和响应处理细节。这种声明式的方式使得使用OpenFeign更加方便和直观。
2.3 Gateway:
当我们解决了微服务直接跨模块调用的问题之后,还需要解决的是与客户端之间调用的问题。
传统的单体架构中只需要开放一个服务给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,如果没有网关,客户端只能在本地记录每个微服务的调用地址,当需要调用的微服务数量很多时,它需要了解每个服务的接口,这个工作量很大。那有了网关之后,能够起到怎样的改善呢?
网关作为系统的唯一流量入口,封装内部系统的架构,所有请求都先经过网关,由网关将请求路由到合适的微服务,所以,使用网关的好处在于:
(1)简化客户端的工作。网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务; (2)降低函数间的耦合度。 一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性 (3)解放开发人员把精力专注于业务逻辑的实现。由网关统一实现服务路由(灰度与ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑
但是 API 网关也存在不足之处,在微服务这种去中心化的架构中,网关又成了一个中心点或瓶颈点,它增加了一个我们必须开发、部署和维护的高可用组件。正是由于这个原因,在网关设计时必须考虑即使 API 网关宕机也不要影响到服务的调用和运行,所以需要对网关的响应结果有数据缓存能力,通过返回缓存数据或默认数据屏蔽后端服务的失败。
在服务的调用方式上面,网关也有一定的要求,API 网关最好是支持 I/O 异步、同步非阻塞的,如果服务是同步阻塞调用,可以理解为微服务模块之间是没有彻底解耦的,即如果A依赖B提供的API,如果B提供的服务不可用将直接影响到A不可用,除非同步服务调用在API网关层或客户端做了相应的缓存。因此为了彻底解耦,在微服务调用上更建议选择异步方式进行。而对于 API 网关需要通过底层多个细粒度的 API 组合的场景,推荐采用响应式编程模型进行而不是传统的异步回调方法组合代码,其原因除了采用回调方式导致的代码混乱外,还有就是对于 API 组合本身可能存在并行或先后调用,对于采用回调方式往往很难控制。
2.3.1 网关分类:
Zuul:
Zuul是Spring Cloud的早期版本中最常用的网关之一。
它支持动态路由、过滤器链和负载均衡。
Zuul 1.x基于阻塞式I/O,而Zuul 2.x计划使用非阻塞式I/O。
Spring Cloud Gateway:
Spring Cloud Gateway是一个基于Spring 5和Project Reactor的新一代网关。
它采用了非阻塞式I/O,使其更适合与反应性微服务一起使用。
Spring Cloud Gateway提供了一种简单而强大的方式来进行路由、过滤和处理请求。
2.3.2 使用:
主要讲解Spring Cloud Gateway的使用。
2.3.2.1 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2.3.2.2 配置:
配置是网关的核心,网关路由转发的核心功能通过配置来实现。
discovery:
locator:
####开启以服务id去注册中心上获取转发地址
enabled: true
###路由策略
routes:
###路由id
- id: auth
#### 基于lb负载均衡形式转发
uri: lb://elevator-auth
###匹配规则
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
###路由id
- id: system
#### 基于lb负载均衡形式转发
uri: lb://elevator-system
###匹配规则
predicates:
- Path=/system/**
filters:
- StripPrefix=1
这段配置是一个典型的Spring Cloud Gateway配置,它定义了多个路由规则,将不同的请求路径转发到相应的微服务上。
locator是服务发现配置,true表示从注册中心上获取转发地址。
每个路由都由三部分组成:
1️⃣ 路由( route ):路由是网关最基础的部分,路由信息由一个 ID ,一个目的 URL 、一组断言工厂和一 组 Filter 组成。如果断言为真,则说明请求 URL 和配置的路由匹配。 2️⃣ 断言( Predicate ): Java8 中的断言函数, Spring Cloud Gateway 中的断言函数输入类型是 Spring5.0 框架中的 ServerWebExchange 。 Spring Cloud Gateway 中的断言函数允许开发者去定义匹配 来自 http Request 中的任何信息,比如请求头和参数等。 3️⃣ 过滤器( Filter ):一个标准的 Spring WebFilter , Spring Cloud Gateway 中的 Filter 分为两种类型: Gateway Filter 和 Global Filter 。过滤器 Filter 可以对请求和响应进行处理。
在上面的配置中:
针对每个微服务,都定义了一个路由规则。
id
: 路由的唯一标识符。uri
: 目标服务的负载均衡地址,使用lb://
前缀表示使用服务注册中心。predicates
: 匹配规则,这里是基于路径匹配。filters
: 过滤器,这里使用StripPrefix=1
表示去掉请求路径的第一个部分,通常用于去掉服务名前缀。#路径前缀删除示例:请求/name/bar/foo,StripPrefix=2,去除掉前面两个前缀之后,最后转发到目标服务的路径为/foo #前缀过滤,请求地址:http://localhost:8084/usr/hello #此处配置去掉1个路径前缀,再配置上面的 Path=/system/**,就将**转发到指定的微服务 #因为这个api相当于是服务名,只是为了方便以后nginx的代码加上去的,对于服务提供者service-client来说,不需要这段地址,所以需要去掉
具体路由规则:
例如,对于名为
auth
的路由规则,它将匹配以/auth/
开头的路径,然后将请求转发到elevator-auth
微服务,同时去掉请求路径的第一个部分(StripPrefix=1
)。
其他路由规则:
类似地,对于其他微服务(如
system
、unit
、order
等),都定义了类似的路由规则。
2.3.2.3 拦截器配置:
网关的配置之只能实现路由转发的功能 想要过滤器还需要自己配置。
下面是一个简单的过滤器配置demo:
主要过滤的不携带token的请求,并对携带token的请求进行验证。
package com.elevator.gateway.security;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.elevator.common.constants.SecurityConstant;
import com.nimbusds.jose.JWSObject;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 网关自定义鉴权管理器
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final RedisTemplate redisTemplate;
@SneakyThrows
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
if (request.getMethod() == HttpMethod.OPTIONS) { // 预检请求放行
return Mono.just(new AuthorizationDecision(true));
}
PathMatcher pathMatcher = new AntPathMatcher(); // 【声明定义】Ant路径匹配模式,“请求路径”和缓存中权限规则的“URL权限标识”匹配
String method = request.getMethodValue();
String path = request.getURI().getPath();
String restfulPath = method + " " + path;
// 如果token以"bearer "为前缀,到此方法里说明JWT有效即已认证
String token = request.getHeaders().getFirst(SecurityConstant.AUTHORIZATION_KEY);
if (StrUtil.isEmpty(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstant.JWT_PREFIX) ) {
return Mono.just(new AuthorizationDecision(false));
}
// 解析JWT
token = StrUtil.replaceIgnoreCase(token, SecurityConstant.JWT_PREFIX, Strings.EMPTY);
String payload = StrUtil.toString(JWSObject.parse(token).getPayload());
JSONObject jsonObject = JSONUtil.parseObj(payload);
// 获取 ClientId
String clientId = jsonObject.getStr(SecurityConstant.CLIENT_ID_KEY);
/**
* 鉴权开始
* 缓存取 [URL权限-角色权限集合] 规则数据
* urlPermRolesRules = [{'key':'GET /admin/user/*','value':['role1', 'role2']},...]
*/
// 判断是否存在权限对应数据
if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
if (!redisTemplate.hasKey(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
if (!redisTemplate.hasKey(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
Map<String, Object> webUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
Set<String> webUrlPermsSet = webUrlPermRolesRules.keySet();
Map<String, Object> appUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
Set<String> appUrlPermsSet = appUrlPermRolesRules.keySet();
Map<String, Object> useUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY);
Set<String> useUrlPermsSet = useUrlPermRolesRules.keySet();
Map<String, Object> maintainUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY);
Set<String> maintainUrlPermsSet = maintainUrlPermRolesRules.keySet();
// 判断端
Map<String, Object> urlPermRolesRules = null;
if (SecurityConstant.ADMIN_WEB_CLIENT_ID.equals(clientId)) {
urlPermRolesRules = webUrlPermRolesRules;
// 如果访问的接口路径在web端没有,禁止访问
if (!webUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
} else if (SecurityConstant.ADMIN_APP_CLIENT_ID.equals(clientId)) {
urlPermRolesRules = appUrlPermRolesRules;
// 如果访问的接口路径在app端没有,禁止访问
if (!appUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
}else if(SecurityConstant.USE_CLIENT_ID.equals(clientId)){
urlPermRolesRules = useUrlPermRolesRules;
// 如果访问的接口路径在使用app端没有,禁止访问
if (!useUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
}else if(SecurityConstant.MAINTAIN_CLIENT_ID.equals(clientId)){
urlPermRolesRules = maintainUrlPermRolesRules;
// 如果访问的接口路径在维保app端没有,禁止访问
if (!maintainUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
}
// 根据请求路径获取有访问权限的角色列表
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
boolean requireCheck = false; // 是否需要鉴权,默认未设置拦截规则不需鉴权
for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
String perm = permRoles.getKey();
if (pathMatcher.match(perm, restfulPath)) {
List<String> roles = Convert.toList(String.class, permRoles.getValue());
authorizedRoles.addAll(roles);
if (!requireCheck) {
requireCheck = true;
}
}
}
// 没有设置拦截规则放行
if (requireCheck == false) {
return Mono.just(new AuthorizationDecision(true));
}
// 判断JWT中携带的用户角色权限是否有权限访问
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authority -> {
String permCode = StrUtil.removePrefix(authority, SecurityConstant.AUTHORITY_PREFIX);
boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(permCode);
return hasAuthorized;
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
return authorizationDecisionMono;
}
}