Gateway
Gateway—SpringCloud微服务网关组件
在微服务架构中,通常一个系统会被拆分为多个微服务,微服务之间的调用可以用OpenFeign,但面对这么多微服务客户端调用会遇到哪些问题呢?
-
每个服务都需要鉴权、限流、跨域访问、权限验证等操作,如果每个微服务各自为战,会很麻烦。
-
对于客户端来说,每个微服务都分配一个域名的话,客户端代码会很难维护,而且连接数也会有瓶颈.
-
随着一个项目的微服务的增多,后期对微服务进行重构的话,也会变的非常麻烦,需要客户端配合一起修改。
为了解决上面的问题,微服务引入了 网关 的概念,网关为微服务架构的系统提供简单、有效且统一的API路由管理,作为系统的统一入口,提供内部服务的路由中转,给客户端提供统一的服务,可以实现一些和业务没有耦合的公用逻辑,主要功能包含认证、鉴权、路由转发、安全策略、防刷、流量控制、监控日志等。
Spring Cloud Gateway 是 Spring Cloud 新推出的网关框架,之前是 Netflix Zuul。
简单来说:Gateway相当于医院大厅的挂号台,对病人进行引流。
加入网关后结构图:
Route(路由): 是构建网关的基本模型, 由ID ,URI 一系列的断言和过滤器组成。
Predicate (断言): 可以匹配Http 请求中所有的内容(请求头 参数等等) 请求与断言,相匹配则通过当前断言。
Filter(过滤器): 包括全局和局部过滤器 ,可以在请求被路由钱后对请求进行更改。
前面我们学习过Nacos,可以帮助我们管理我们的服务,学习Spring Cloud Gateway时,我们可以直接将Nacos整合进来。
复制之前的Sentinel项目,在此基础上添加api-gateway子模块
添加入口类
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--由于springcloud2020弃用了Ribbon,因此Alibaba在2021版本nacos中删除了Ribbon的jar包,因此无法通过lb路由到指定微服务,会出现503情况。需要引入springcloud loadbalancer包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--其它包-->
<!-- 服务注册与发现 jar包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- nacos配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 识别bootstrap.yml文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.0.2</version>
</dependency>
GATEWAY-dev.yml
server:
port: 9000
spring:
application:
name: GATEWAY
cloud:
loadbalander: #注意这里要排除ribbon
ribbon:
enable: false
gateway:
routes:
- id: search_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
filters:
- StripPrefix=1
注解: http://localhost:9000/search-service/goods http://localhost:8083/goods
routes: 路由/路由数组 当请求满足指定的条件后转发到哪个微服务上
id: 当前路由唯一的标识符 ,有默认值,也可以自定义
uri: 请求最终要被转到的地址 ,lb为load balance,表示负载均衡,比如lb://SEARCH表示请求最终会转发到SEARCH服务,注意 lb://后面的<clientName>一定不要使用下划线,即不要使用比如:my_service这种形式,否则LoadBanancer不会起作用。
predicates:断言,也就是条件判断,- Path=/search-service/表示当客户端访问 http://localhost:9000/search-service/goods时会路由到http://localhost:8083/search-service/goods(我这里的SEARCH路径地址为http://localhost:8083),这个无法访问,需要filters过滤下
filters: 过滤器,在请求传递过程中,对请求做一下处理,比如添加请求头,去掉部分路径等。- StripPrefix=1表示转发之前去掉第一层路由,也就是转发到http://localhost:8083/goods
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
config:
server-addr: http://localhost:8848
namespace: dev
group: DEFAULT_GROUP
username: nacos
password: nacos
prefix: GATEWAY
file-extension: yml
shared-configs:
- common.yml
config:
activate:
on-profile: dev
修改GATEWAY-dev.yml配置信息为:
server:
port: 9000
spring:
application:
name: GATEWAY
cloud:
nacos:
discovery:
server-addr: localhost:8848
loadbalander:
ribbon:
enable: false
gateway:
discovery:
locator:
enabled: true
spring.cloud.gateway.discovery.locator.enabled=true,表示会拿nacos服务名为断言,也会将服务名进行过滤,从而路由到该服务的请求。所以访问:http://localhost:9000/SEARCH/goods 会路由到http://localhost:8083/goods
断言就是在进入网关请求之前所做的操作,也就是先执行的程序,和生活中坐车一样,首先必须进行安检,然后查票,验证票最终才可以坐车。
在这里的理解就是:当满足某种条件后才会被转发,如果是多个,那就是都满足的情况下被转发。
Spring Cloud Gateway可以匹配各种路由,而其内部就包括许多内置的路由断言工厂。所有这些断言都匹配HTTP请求的不同属性。您可以将多个路由断言工厂与逻辑和语句组合在一起使用。
Path是最常见的断言请求,匹配指定路径下的请求,可以是具体的请求,也可使用/**表示匹配所有子级请求,配置如下。
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
配置中匹配了以/search-service开头的请求,如果是其他URL的请求进入系统,会出现错误。
After Route Predicate(匹配时间后的请求)
After Route Predicate可以匹配ZonedDateTime类型的时间,表示:匹配在指定日期时间之后发生的请求,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- After=2022-07-8T14:00:00+08:00[Asia/Shanghai]
配置中匹配了2022-07-8 14:00:00后的请求,如果是在指定时间之前进入系统的请求,会出现错误。
Before Route Predicate(匹配时间前的请求)
Before Route Predicate可以匹配ZonedDateTime类型的时间,表示:匹配在指定日期时间之前发生的请求,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- Before=2022-07-8T14:00:00+08:00[Asia/Shanghai]
配置中匹配了2022-07-8 14:00:00之前的请求,如果是在指定时间之后进入系统的请求,会出现错误。
Between Route Predicate(匹配时间之间的请求)
Between Route Predicate可以匹配ZonedDateTime类型的时间,由两个ZonedDateTime参数组成,第一个参数为开始时间,第二参数为结束时间,表示:匹配在指定的开始时间与结束时间之内发生的请求,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- Between=2022-07-8T14:00:00+08:00[Asia/Shanghai],2022-07-28T14:00:00+08:00[Asia/Shanghai]
配置中匹配2022-07-8 14:00:00到2022-07-28 14:00:00之内时间段的请求,如果是在指定时间段外的进入系统的请求,会出现错误。
CookieRoutePredicate由两个参数组成,第一个参数为cookie的Key,第二参数为cookie的Value,表示:匹配指定名称且其值与正则表达式匹配的cookie的请求,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- Cookie=cookieName, \d+
配置中匹配了cookie的Key为cookieName,值为满足\d+的正则表达式请求,如果满足cookieName不满足\d+的请求,会出现错误。
HeaderRoutePredicate由两个参数组成,第一个参数为Header名称,第二参数为Header的Value值,表示:匹配指定名称且其值与正则表达式匹配的Header的请求,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- Header=X-Request-Id, \d+
配置中匹配了Header的名称为X-Request-Id,值为满足\d+的正则表达式请求,如果满足headerName不满足\d+的请求,会出现错误。
HostRoutePredicate参数为请求的Host地址,多个参数使用逗号分割,设置的Host地址可以使用**表示通配符,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Host=**.test1.com,**.test2.com
配置中匹配的Host,可以匹配以test1.com或者test2.com结尾的Host地址,其他Host地址访问会出现错误。
MethodRoutePredicate由一个或多个HTTP Method组成,比如:POST、PUT、GET、DELETE,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- Method=GET,POST
配置中匹配了HTTP Method的类型为GET和POST,如果是其他类型的HTTP Method,会出现错误。
QueryRoutePredicate由两个参数组成,第一个参数为参数名称,第二参数为参数的值(满足正则即可),表示:匹配指定名称且其值与正则表达式匹配的带参的请求,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- Query=name,\d+
配置中匹配了参数名称叫做name,值满足\d+的请求,如果不满足\d+,会出现错误。
RemoteAddrRoutePredicate的参数由CIDR 表示法(IPv4 或 IPv6)字符串组成,配置如下:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- RemoteAddr=192.168.1.1/24
配置中可以匹配IP为192.168.1.1–192.168.1.254的值,如果不满足192.168.1.1/24的IP规则,会出现错误。
WeightAddrRoutePredicate由group和weight(权重数值)组成,表示将相同的请求根据权重跳转到不同的uri地址,要求group的名称必须一致,配置如下:
spring:
cloud:
gateway:
locator:
enabled: true
routes:
-id: weight_route1
uri: lb://SEARCH
predicates:
- Path=/weight/**
- Weight= group3, 1
-id: weight_route2
uri: lb://USERS
predicates:
- Path=/weight/**
- Weight= group3, 9
如上配置了两个对于 / weight/** 路径转发的路由定义,这两个路由是同一个权重分组,且 weight_ route1 权重为 1, weight_ route2 权重为9。 对于10个访问/ weight/** 路径的请求来说,将会有9个路由到 weight_ route2,1个路由到 weight_ route1。
假设我们需要对访问的用户年龄做限制,只允许18-60岁之间的人来访问。我们可以自己定义断言来实现
package com.test.apigateway.Predicate;
import com.alibaba.cloud.commons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
@Component
// 自定义路由断言工厂
public class MyAgeRoutePredicateFactory extends AbstractRoutePredicateFactory<MyAgeRoutePredicateFactory.Config> {
public MyAgeRoutePredicateFactory() {
super(MyAgeRoutePredicateFactory.Config.class);
}
// 将配置文件中的值按返回集合的顺序,赋值给配置类
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(new String[]{"minAge", "maxAge"});
}
@Override
public Predicate<ServerWebExchange> apply(Consumer<Config> consumer) {
return super.apply(consumer);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// 创建网关断言对象
return new Predicate<ServerWebExchange>() {
// 检查
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 获取请求参数age,判断是否满足[18, 60)
MultiValueMap<String, String> queryParams = serverWebExchange.getRequest().getQueryParams();
String age = queryParams.getFirst("age");
if (!StringUtils.isEmpty(age) && age.matches("[0-9]+")) {
int iAge = Integer.parseInt(age);
if (iAge >= config.minAge && iAge < config.maxAge) {
return true;
}
}
return false;
}
};
}
// 配置类,属性用于接收配置文件中的值
@Validated
public static class Config {
private int minAge;
private int maxAge;
public int getMinAge() {
return minAge;
}
public void setMinAge(int minAge) {
this.minAge = minAge;
}
public int getMaxAge() {
return maxAge;
}
public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
}
}
要求:
类必须要加上RoutePredicateFactory作为结尾
类必须继承AbstractRoutePredicateFactory
必须声明静态内部类。
gateway:
routes:
- id: myage
uri: lb://SEARCH
predicates:
- Path=/search-service/**
- MyAge=18,60
filters:
- StripPrefix=1
MyAge即是我们新建断言工厂的前缀名,自动识别的。
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
是指作用在某一个路由上,SpringCloud Gateway内置了很多路由过滤器,他们都是由GatewayFilter的工厂类产生。
比如AddRequestParameter GatewayFilter
该过滤器可以给请求添加参数。
比如我在SEARCH服务有一个带有page参数的接口,我想请求网关路由转发的时候给加上一个page=1的参数。
接口如下:
添加配置信息:
测试
再比如前面用过的- StripPrefix=1,表示把请求网关的路径前缀的第一级去掉。
内置过滤器很多,我们这里举一个例子看一下使用方式,其它的可以看帮助手册
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
全局过滤器在系统初始化时就作用于所有的路由,不需要单独去配置。全局过滤器的接口定义类是GlobalFilter,Gateway本身也有很多内置的过滤器。
比如LoadBalancerClientFilter
该过滤器会解析到以lb://开头的uri,比如这样的配置:
gateway:
routes:
- id: abc
uri: lb://SEARCH
predicates:
- Path=/search-service/**
filters:
- StripPrefix=1
- AddRequestParameter=page,1
它会使用Spring Cloud的LoadBalancerClient 来将 SEARCH服务解析成实际的host和port,重新组装请求的url。
它是作用于全局,而且并不需要配置。
启用Reactor Netty访问日志:-Dreactor.netty.http.server.accessLogEnabled=true
测试:
<!-- 导入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 导入sentinel整合Gateway-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
sentinel:
transport:
port: 8888
dashboard: http://localhost:8080
启动Sentinel,重启网关微服务,运行一次服务中的接口,再次刷新Sentinel,会出现以下内容,表示整合成功
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
route维度:即在配置文件中配置的路由条目,资源名为对应的routeId,这种属于粗粒度的限流,一般是对某个微服务进行限流。
自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组,这种属于细粒度的限流,针对某一类的uri进行匹配限流,可以跨多个微服务。
这里的Burst size是指:应对突发请求时额外允许的请求数目。
还可以设置针对断言的请求属性:
添加API分组:
对当前分组限流:
测试:
属于API分组的路由被限流
不在分组内的不受影响