文章目录
1、网关简介
1.1、为什么需要网关?
- 大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务
- 那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,分别去调用
如下图所示:
这样的架构,会存在着诸多的问题:
- 客户端多次请求不同的微服务,客户端需要配置所有微服务的IP端口等信息,增加客户端代码或配置编写的复杂性
- 认证复杂,每个服务都需要独立认证
- 存在跨域请求,在一定场景下处理相对复杂
针对上面的这些问题,我们可以借助API网关来解决
1.2、网关概念
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等
添加上API网关之后,系统的架构图变成了如下所示:
现在的整体架构应该是这样了:
由上图我们可以总结一下:
- API网关作用就是:把各个服务对外提供的API汇聚起来,让外界看起来是一个统一的接口。同时也可在网关中提供额外的功能
- 一句话:网关就是所有项目的一个统一入口
- 作用有两个:路由转发和过滤器
路由转发
接收外界请求,通过网关的路由转发,转发到后端的服务上
过滤器
对于我们来说比较常用的功能有鉴权、限流、路由、监控、日志记录等功能
1.3、网关的分类
在业界比较流行的网关,有下面这些
1、Nginx+Lua
- 使用nginx的反向代理和负载均衡可实现对 api服务器 的负载均衡及高可用
- lua是一种脚本语言,可以来编写一些简单的逻辑,nginx支持lua脚本
2、Kong
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用
问题:
- 只支持Http协议
- 二次开发,自由扩展困难
- 提供管理API,缺乏更易用的管控、配置方式
3、Zuul
- Spring Cloud Netflix Zuul:属于Spring Cloud Netflix下一个组件,具有灵活、简单的特点
- 在早期Spring Cloud中使用的比较多,其版本更新依赖于Netflix Zuul
版本说明:
1、Zuul1.0
Zuul1.0和Zuul2.0官网地址:https://github.com/Netflix/zuul/wiki
Zuul1.0的生命周期:
2、Zuul2.0
Zuul2.0的生命周期:
两者相比,主要有两点区别:
- 前端用Netty Server代替Servlet,目的是支持前端异步。后端用Netty Client代替Http Client,目的是支持后端异步
- 过滤器换了一下名字,用Inbound Filters代替Pre-routing Filters,用Endpoint Filter代替Routing Filter,用Outbound Filters代替Post-routing Filters
两者的应用场景不同:
Zuul 1 (阻塞)的应用场景:
- cpu密集型任务
- 简单操作的需求
- 开发简单的需求
- 实时请求高的
Zuul 2(非阻塞)的应用场景:
- io密集的任务
- 大请求或者大文件
- 队列的流式数据
- 超大量的连接
4、Gateway
- Spring Cloud Gateway:由Spring 自己推出的网关产品,完全依赖Spring自家产品
- 符合Spring战略意义,其更新版本等都由Spring自己把控
注意点:
- 目前很多项目中都使用Gateway替代Zuul,而且Gateway的性能也优于Zuul
- Spring Cloud Alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关
2、Gateway简介
2.1、Gateway概念
- Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式
- 它的目标是替代Netflflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流
目前版本:https://spring.io/projects/spring-cloud-gateway#learn,如下图所示:
2.2、优缺点
优点:
- 性能强劲:是第一代网关Zuul的1.6倍
- 功能强大:内置了很多实用的功能,例如转发、监控、限流等
- 设计优雅,容易扩展
缺点:
- 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
- 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
- 需要Spring Boot 2.0及以上的版本才支持
3、Gateway快速入门
需求:通过浏览器访问api网关,然后通过网关将请求转发到用户服务
3.1、基础版
1、创建微服务
创建一个子模块,名字叫:springcloudalibaba-gateway-server-1040,它是一个微服务,然后pom.xml导入下面依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringCloudAlibabaDemo</artifactId>
<groupId>cn.wujiangbo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springcloudalibaba-gateway-server-1040</artifactId>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
</project>
注意:gateway网关服务中不需要引入【spring-boot-starter-web】依赖,否则会报如下错误,因为【spring-cloud-starter-gateway】依赖中已经包含这个web包了:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'routeDefinitionRouteLocator' defined in class path resource [org/springframework/cloud/gateway/config/GatewayAutoConfiguration.class]: Unsatisfied dependency expressed through method 'routeDefinitionRouteLocator' parameter 4; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.convert.ConversionService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=webFluxConversionService)}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:769) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:509) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1320) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1159) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:847) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1204) [spring-boot-2.1.13.RELEASE.jar:2.1.13.RELEASE]
at cn.wujiangbo.GatewayApp1030.main(GatewayApp1030.java:17) [classes/:na]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.convert.ConversionService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=webFluxConversionService)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1662) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1221) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1175) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760) ~[spring-beans-5.1.14.RELEASE.jar:5.1.14.RELEASE]
... 19 common frames omitted
2、启动类
package cn.wujiangbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* <p>启动类</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@SpringBootApplication
public class GatewayApp1040 {
public static void main(String[] args){
SpringApplication.run(GatewayApp1040.class, args);
}
}
3、yml配置
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://localhost:1010 # 请求要转发到的地址
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
UserController类如下:
package cn.wujiangbo.controller;
import cn.wujiangbo.dto.User;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>用户服务相关api接口</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@RestController
@RequestMapping("/user")
public class UserController {
//获取配置文件中的值
@Value("${server.port}")
private String port;
@GetMapping("/getUserById/{id}")
//限流降级
@SentinelResource(value="getUserById", blockHandler="exceptionHandler", fallback = "getUserByIdFallback")
public User getUserById(@PathVariable Long id){
return new User(id,"王天霸", "我是王天霸,你好吗?port=" + port);
}
// 限流与阻塞处理 : 参数要和 被降级的方法参数一样
public User exceptionHandler(@PathVariable("userId") Long userId, BlockException ex) {
ex.printStackTrace();
return new User(-1L,"null","抱歉,Sentinel-限流");
}
// 熔断降级,参数和返回值与源方法一致
public User getUserByIdFallback(@PathVariable("userId") Long userId){
return new User(userId,"null", "抱歉,Sentinel-熔断");
}
}
4、测试
启动用户服务和网关服务,浏览器访问网关服务:http://localhost:1040/user-server/user/getUserById/13,页面结果:
达到预期效果,测试成功
3.2、增强版
上面yml文件中我们看到了,uri的转发目标地址是硬编码写死的,那万一用户服务做了集群的话,这里就不好办了,这样很不优雅
所以接下来我们从注册中心获取此转发目标地址:
1、修改pom文件
pom.xml添加nacos依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringCloudAlibabaDemo</artifactId>
<groupId>cn.wujiangbo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springcloudalibaba-gateway-server-1040</artifactId>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册与发现-->
<dependency>
<groupId>com.alibaba.cloud </groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
2、修改启动类
启动类上需要添加@EnableDiscoveryClient注解,如下:
package cn.wujiangbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* <p>启动类</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApp1040 {
public static void main(String[] args){
SpringApplication.run(GatewayApp1040.class, args);
}
}
3、修改yml
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
discovery:
locator:
enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
# uri: http://localhost:1010 # 请求要转发到的地址
uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
4、测试
启动用户服务和网关服务,浏览器访问网关服务:http://localhost:1040/user-server/user/getUserById/13,页面结果:
达到预期效果,测试成功
3.3、简写版
1、修改yml
去掉关于路由的配置:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
discovery:
locator:
enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
2、测试
启动用户服务和网关服务,浏览器访问网关服务:http://localhost:1040/user-server/user/getUserById/13,页面结果:
达到预期效果,测试成功,只要按照网关地址/微服务/接口的格式去访问,就可以得到成功响应
4、Gateway核心架构
4.1、基本概念
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体
主要定义了下面的几个信息:
- id,路由唯一标识符,区别于其他 Route
- uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务
- order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高
- predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由
- fifilter,过滤器用于修改请求和响应信息
4.2、执行流程
执行流程大体如下:
- Gateway Client向Gateway Server发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
- RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
- 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
- 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应
4.3、断言
- Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由
- 断言的作用:在 什么条件下 才能进行路由转发
1、内置路由断言工厂
- Spring Cloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配
- predicates中的每一个配置官方都称之为路由断言工厂
- 它的作用就是:当请求gateway的时候,使用断言对请求进行匹配,如果匹配成功就路由转发,如果匹配失败就返回404
路由断言工厂分为下面两种:
- 内置路由断言工厂,也就是官方已经定义好了,我们直接使用的
- 自定义路由断言工厂,需要我们自定义
内置路由断言工厂具体有下面这些:
1、基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:
- AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
- BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
- BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
案例:
# 当前的请求必须要在下方指定的时间之后
- After=2022-12-20T17:42:47.789-07:00[America/Denver]
# 当前的请求必须在下方指定的时间之前
- Before=2022-12-20T17:42:47.789-07:00[America/Denver]
# 当前的请求必须在下方指定的时间段之内
- Between=2022-12-20T17:42:47.789-07:00[America/Denver],2022-12-25T17:42:47.789-07:00[America/Denver]
这个时间格式是带区域的,以后如果忘记了可以使用ZonedDateTime.now()
来输出
2、基于远程地址的断言工厂
RemoteAddrRoutePredicateFactory,接收一个IP地址段,判断请求主机地址是否在地址段中
案例:
-RemoteAddr=192.168.1.1/24
3、基于Cookie的断言工厂
CookieRoutePredicateFactory,判断请求Cookie中必须某个key对应的value必须为指定的值,接收两个参数,cookie 名字和一个正则表达式
案例:
# cookie中 TestToken 的值必须为colin.wjb 第二个参数的值可以使用正则表达式
- Cookie=TestToken,colin.wjb
4、基于Header的断言工厂
HeaderRoutePredicateFactory,接收两个参数,标题名称和正则表达式
判断请求Header是否具有给定名称且值与正则表达式匹配
案例:
#这个路由规则匹配Header中包含X-Request-Id并且值为纯数字的请求
-Header=X-Request-Id, \d+
5、基于Host的断言工厂
HostRoutePredicateFactory,接收一个参数,主机名模式
判断请求的Host是否满足匹配规则
案例:
#这个路由规则匹配Header中必须包含.ybz.com
-Host=**.ybz.com
6、基于Method请求方法的断言工厂
MethodRoutePredicateFactory,接收一个参数,判断请求类型是否跟指定的类型匹配
案例:
##限制请求方式为GET或者POST
-Method=GET,POST
7、基于Path请求路径的断言工厂
PathRoutePredicateFactory,接收一个参数,判断请求的URI部分是否满足路径规则
案例:
# 当请求路径满足Path指定的规则时,才进行路由转发
- Path=/user-server/**
8、基于Query请求参数的断言工厂
QueryRoutePredicateFactory ,接收两个参数,请求param和正则表达式
判断请求参数是否具有给定名称且值与正则表达式匹配
案例:
-Query=abc,def.
解读:abc请求参数名称,def. 是abc的值,是一个正则表达式,在正则表达式中点(.)表示匹配任意一个字符,所以当请求参数abc=defaaa或abc=defbbb能满足断言条件
9、基于路由权重的断言工厂
WeightRoutePredicateFactory,接收一个[组名,权重],然后对于同一个组内的路由按照权重转发
案例:
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weight.high.org
predicates:
- Weight=group1,8 #80%
- id: weight_low
uri: https://weight.low.org
predicates:
- Weight=group1,2 #20%
解读:该路由会将约 80% 的流量转发到:weight.high.org,将约 20% 的流量转发到:weight.low.org
2、内置路由断言工厂的使用
接下来我们验证几个内置断言的使用,yml改成这样:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
# discovery:
# locator:
# enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://127.0.0.1:1010
# uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
# - Before=2021-11-28T00:00:00.000+08:00 #限制请求时间在 2022-11-28 之前
- Method=POST #限制请求方式为POST
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉第1层路径
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
logging:
level:
org.springframework.cloud.gateway: debug
特别坑的地方:
predicates下面用哪些断言工厂配置(比如Before、Method等)时,不能使用enabled:true的配置,否则会失效
好,我们开始测试
启动用户服务和网关服务,浏览器访问:http://localhost:1040/user-server/user/getUserById/13,页面结果:
为什么会报这个错呢?
控制台打印如下:
2022-10-20 15:19:51.034 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition user_route applying {_genkey_0=/user-server/**} to Path
2022-10-20 15:19:51.035 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition user_route applying {_genkey_0=POST} to Method
2022-10-20 15:19:51.035 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition user_route applying filter {_genkey_0=1} to StripPrefix
2022-10-20 15:19:51.036 DEBUG 13784 --- [ctor-http-nio-3] o.s.c.g.r.RouteDefinitionRouteLocator : RouteDefinition matched: user_route
提示很明显,因为断言配置了:- Method=POST,含义是只有POST请求才会转发,其他请求不处理,通过浏览器直接访问,属于GET请求,所以不会转发
3、自定义路由断言工厂
1、介绍
- 内置路由断言工厂已经能够满足日常大部分的业务需求了,但是如果出现了某些业务场景无法满足的情况就需要进行自定义路由断言工厂了
- 自定义路由断言工厂需要继承
AbstractRoutePredicateFactory
类,重写apply()
方法的逻辑 - 在
apply()
方法中可以通过exchange.getRequest()
拿到ServerHttpReqeust
对象,从而可以获取到请求参数、请求方式、请求头等信息
编码注意事项:
- 必须注册进spring容器中
- 类的命名需要以
RoutePredicateFactory
结尾 - 需要继承
AbstractRoutePredicateFactory
- 必须在类中创建一个静态内部类Config,并声明成员变量,它的作用就是用来接收yml配置文件中的配置信息的
- 在重写的
shortcutFieldOrder()
方法中,它会把yaml配置文件中写的值,映射到静态内部类Config中的属性 - 在重写的apply()方法中实现业务逻辑,返回一个Boolean类型的值,true就是断言匹配成功,false就是匹配失败
2、实战
需求是:前端请求时传参age字段的值,该值必须在我yml中设置的范围之中,否则就断言失败
先自定义一个断言工厂,实现断言方法
package cn.wujiangbo.predicates;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* <p>自定义断言工厂</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@Component
//泛型 用于接收一个配置类,配置类用于接收中配置文件中的配置
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值,赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言,如果返回true就是断言匹配成功,返回false就是匹配失败
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new Predicate<ServerWebExchange>(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//从serverWebExchange获取传入的参数
String ageString = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if(StringUtils.hasLength(ageString)){
//如果前端传值的话
int age = Integer.parseInt(ageString);
//大于最小值且小于最大值,那就成功返回true
return age > config.getMinAge() && age < config.getMaxAge();
}
return true;
}
};
}
//创建一个静态内部类,成员变量的set/get方法必须要
@Data
public static class Config{
private int minAge;//最小年龄
private int maxAge;//最大年龄
}
}
然后在yml中设置age字段值的范围,如下:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
# discovery:
# locator:
# enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://127.0.0.1:1010
# uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
# - Before=2022-11-28T00:00:00.000+08:00 #限制请求时间在 2022-11-28 之前
# - Method=POST,GET #限制请求方式为POST或者GET
- Age=18,30 # 限制年龄只有在18到30岁之间的人能访问
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉第1层路径
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
logging:
level:
org.springframework.cloud.gateway: debug
开始测试:
浏览器分别访问下面两个地址:
- http://localhost:1040/user-server/user/getUserById/13?age=19 – 可以正常拿到数据
- http://localhost:1040/user-server/user/getUserById/13?age=17 – 页面报404的错误,如下:
4.4、过滤器
1、介绍
掌握下面三个知识点:
- 过滤器的作用:过滤器就是在请求的传递过程中,对请求和响应做一些手脚
- 生命周期:分为Pre和Post
- 分类:分为局部过滤器(作用在某一个路由上)和全局过滤器(作用全部路由上)
在Gateway中,Filter的生命周期只有两个:“pre” 和 “post”
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter
- GatewayFilter:应用到单个路由或者一个分组的路由上
- GlobalFilter:应用到所有的路由上
2、局部过滤器
局部过滤器是针对单个路由的过滤器
1、内置局部过滤器
官方文档,详细介绍了很多的过滤器工厂,这里就拿一些常用的记录
添加请求头
# 添加一个请求头X-Request-red,值为blue
- AddRequestHeader=X-Request-red, blue
微服务接口中获取请求头数据,代码如下:
@RequestMapping("/filter")
public String filter(@RequestHeader("X-Request-red") String red){
return red;
}
添加请求参数
# 添加一个请求参数red,值为blue
- AddRequestParameter=red, blue
微服务接口中获取请求头数据,代码如下:
@RequestMapping("/filter")
public String filter(@RequestParam("red") String red){
return red;
}
为匹配的路由统一添加前缀
# 添加前缀,对应微服务需要配置context-path
- PrefixPath=/colin
微服务的yaml配置文件中需要添加context-path
server:
port: 1010
servlet:
context-path: /colin
添加响应头
- AddResponseHeader=X-Response-Red, Blue
截断原始请求的路径
# 使用数字表示要截断路径的数量
- StripPrefix=2
重定向
# 重定向到百度
- RedirectTo=302, https://www.baidu.com/
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器
具体如下:
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名及值 |
AddResponseHeader | 添加响应头 | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的header名称及去重策略 |
Hystrix | 为路由引入Hystrixd 的断路器保护 | HystrixCommand的名称 |
FallbackHeaders | 为fallbackUri的请求头添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始的请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求天一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver rateLimiter statusCode denyEmptyKey emptyKeyStatus |
RedirectTo | 重定向到指定的URL | http状态码及重定向的URL |
RemoveRequestHeader | 删除某个请求头 | header名称 |
RemoveResponseHeader | 删除某个响应头 | header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则 |
RewriteResponseHeader | 重写响应头 | Header名称、值的正则表达式、重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
SecureHeaders | 为原始响应添加一系列安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetRequestHeader | 修改请求头 | Header的名称及值 |
SetResponseHeader | 修改响应头 | Header的名称及值 |
SetStatus | 修改响应状态码 | http状态码,可以是数字/字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断路径的数量 |
Retry | 针对不同的响应进行重试 | retries statuses methods series |
RequestSize | 设置允许接收最大请求包的大小。如果超过则返回413 | 请求包大小,单位为字节 默认为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
2、自定义局部过滤器
注意事项:
- 必须注册到spring容器中
- 继承
AbstractNameValueGatewayFilterFactory
抽象类 - 自定义类的名称必须以
GatewayFilterFactory
结尾
我们来写一个试试,需求:灵活配置是否需要在过滤器中校验token
我们先来写一个自定义的Filter,如下:
package cn.wujiangbo.filter;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* <p>自定义过滤器工厂</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@Component
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {
private static final String AUTHORIZE_TOKEN = "token";
public AuthorizeGatewayFilterFactory() {
super(Config.class);
}
//用于从配置文件中获取参数值,赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("tokenEnabled");
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter(){
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!config.tokenEnabled) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null) {
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
if (StringUtils.isEmpty(token)) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "token is Empty"));
}
/**
* 此时需要根据 token 查询Redis,看是否可以查到用户权限信息了
* 我这里做测试,没有引入Redis,所以模拟从Redis查询到数据了
*/
String authTokenFromRedis = "user:zhangsan,age=21";
if (StringUtils.isEmpty(authTokenFromRedis)) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "token is error"));
}
//授权信息不为空,那就放行(真实项目中不会这么简单的,这里是测试)
return chain.filter(exchange);
}
};
}
//创建一个静态内部类,成员变量的set/get方法必须要
@Data
public static class Config {
// 控制是否开启认证
private Boolean tokenEnabled;
}
}
在application.yml配置使用:
server:
port: 1040
spring:
application:
name: gateway-server
cloud:
#网关配置
gateway:
# discovery:
# locator:
# enabled: true #这个一定要配置,默认是false,改成true后,目的是让Gateway服务可以发现Nacos中的微服务
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user_route # 当前路由的标识,要求唯一
uri: http://127.0.0.1:1010
# uri: lb://user-server # lb指的是是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user-server/** # 当请求路径满足Path指定的规则时,才进行路由转发
# - Before=2022-11-28T00:00:00.000+08:00 #限制请求时间在 2022-11-28 之前
# - Method=POST,GET #限制请求方式为POST或者GET
- Age=18,30 # 限制年龄只有在18到30岁之间的人能访问
filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉第1层路径
# 关键在下面一句,值为true则开启认证,false则不开启
- Authorize=true
# nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848 #注册中心地址
logging:
level:
org.springframework.cloud.gateway: debug
开始测试,浏览器分别访问下面两个地址:
- http://localhost:1040/user-server/user/getUserById/13?token=17 – 可以正常拿到数据
- http://localhost:1040/user-server/user/getUserById/13 – 页面报401的错误,如下:
3、全局过滤器
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能
1、内置全局过滤器
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
2、自定义全局过滤器
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验
开发中的鉴权逻辑如下:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 服务端认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效
下图是大概流程:
如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验
检验的标准就是请求中是否携带token凭证以及token的正确性
下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”,则不转发路由,否则执行正常的逻辑,代码如下:
package cn.wujiangbo.filter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* <p>自定义全局过滤器</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鉴权失败");
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication Failed"));
}
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
开始测试:
浏览器访问下面地址:
- http://localhost:1040/user-server/user/getUserById/13 – 页面报401的错误,如下:
用postman测试,在请求头中传一个token的话,就可以正常访问了,如下:
4.5、跨域处理
前后端分离的项目,前端访问后台服务肯定是有跨域的问题的,SpringCloud Gateway提供了处理跨域的功能
1、方式一
通过yml配置文件的方式:
spring:
cloud:
gateway:
# 跨域的配置
globalcors:
cors-configurations:
# /**代表允许跨域访问的资源
'[/**]':
# 下面就是跨域允许的来源,在开发环境就可以直接设置一个*表示所有都可以进行访问
# 一旦开启了跨域,我们只会针对已知的来源允许跨域,可以写一个域名
allowedOrigins: "*"
# 设置允许的请求方式
allowedMethods:
- GET
- POST
2、方式二
通过配置类的方式:
package cn.wujiangbo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* <p>跨域处理</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter(){
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*"); // 允许的请求方式 POST、GET
config.addAllowedOrigin("*"); // 允许的来源
config.addAllowedHeader("*"); // 允许的请求头参数
// 允许访问的资源
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
5、网关限流
5.1、概念介绍
- 网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前面学过的Sentinel组件来实现网关的限流
- Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流
- route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
- 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组
5.2、编码实战
1、导入依赖
网关服务导入下面依赖:
<!--限流依赖-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
2、编写配置类
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可
代码如下:
package cn.wujiangbo.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* <p>限流配置类</p>
*
* @author 波波老师(微信 : javabobo0513)
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
//初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
/**
* 下面设置含义:
* 2秒内最多允许3个请求访问 user_route 服务
* 请求超过了这个阈值的话,就返回下面 initBlockHandlers方法 定义的异常信息
*/
new GatewayFlowRule("user_route") //资源名称,对应路由id
.setCount(3) // 限流阈值
.setIntervalSec(2) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler
sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", "-1");
map.put("message", "接口被限流了(网关限流)");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
3、测试
启动用户服务和网关服务,页面访问:http://localhost:1040/user-server/user/getUserById/13,第一次肯定是可以正常返回信息的,刷新频率快一点,保证2秒内请求次数多余3次就会出现下面提示:
6、总结
- 本文主要介绍了Gateway网关组件的一些使用方式
- 希望大家动起手来写几个Demo测试一下,感受一下Gateway的魅力所在
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116629.html