- 4. gateway 服务网关
- 4.1 什么是网关
- 4.2 gateway简介
- 4.3 构建工程
- 断言">4.4 断言
- 4.4.1 After Route Predicate Factory
- 4.4.2 Before Route Predicate Factory
- 4.4.3 Between Route Predicate Factory
- 4.4.4 Cookie Route Predicate Factory
- 4.4.5 Header Route Predicate Factory
- 4.4.6 Host Route Predicate Factory
- 4.4.7 Method Route Predicate Factory
- 4.4.8 Path Route Predicate Factory
- 4.4.9 Query Route Predicate Factory
- 4.4.10 The RemoteAddr Route Predicate Factory
- 4.4.11 The Weight Route Predicate Factory
- 4.4.12 自定义断言
- 过滤">4.5 过滤
- 4.6 限流
4. gateway 服务网关
4.1 什么是网关
在一个大型项目中,一个系统会被拆分成很多微服务,如果从外部入口直达每个微服务就会有诸多问题
- 客户端需要在每个微服务进行认证
- 跨域请求,存在一定复杂性
- 客户端出现代码和配置重复
所以有了网关这种东西, 就是在服务前面有一个统一入口,将一些与业务无关的公共逻辑封装到里面包括认证、鉴权、监控、路由、限流等等。
4.2 gateway简介
Spring Cloud Gateway是Spring家族开发代替zuul的统一API路由管理方式。并且基于Filter提供网关基本功能,安全,监控,限流。
4.2.1 Route
路由:就是字面意思,满足条件后转发到某个微服务,一个gateway可以包含多个路由。
4.2.2 Predicate
断言:转发前的特定条件
4.2.3 Filter
过滤:在数据传递过程中通过 过滤器做一些事情。
4.2.4 流程
客户端访问 gateway网关,gateway中Handler Mappeer对url进行处理。处理后交给Web Handler ,Web Handler 会被Filter进行过滤,Filter前半部分会对请求的部分进行过滤,请求后被真实服务代理响应返回结果,Filter后半部分回对结果进行操作,然后给Web Handler,再返回给Handler Mapper。最终返回给客户端。
4.3 构建工程
可以单独使用gateway 也可以将gateway和其他服务一起注册到注册中心 然后从注册中心自动获取地址
4.3.1 动态路由 自动获取URL
加入负载均衡组件 可以自动轮询服务
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>cloudalibaba</artifactId>
<groupId>com.rem</groupId>
<version>2021</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>
bootstrap.yml
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
namespace: 3d0a77b8-817f-499b-bfda-f90d5a6e4dab
server-addr: 192.168.19.128:13306
group: DEFAULT_GROUP
username: nacos
password: nacos
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: provider-product # 当前路由的标识, 要求唯一
uri: lb://provider-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略 #http://127.0.0.1:7001
predicates: # 断言(就是路由转发要满足的条件)
- Path=/product/** # 当请求路径满足Path指定的规则时,才进行路由转发
当请求 http://127.0.0.1:9000/product/test/1 的时候就会自动轮询7001和7002两个product服务
4.3.2 动态路由 服务名称转发
新增动态配置
gateway:
discovery:
locator:
#是否与服务发现相结合,通过serviceId转发到具体服务
enabled: true
这样就不需要配置routes的配置 不需要直接写微服务实际地址直接根据服务名就可以,通过服务名称访问
http://127.0.0.1:9000/provider-product/product/test/1
4.4 断言
4.4.1 After Route Predicate Factory
接收一个日期参数,判断请求日期是否晚于指定日期,访问时间在2020-12-31 23:59:59之后所有请求都通过
spring:
cloud:
gateway:
routes:
- id: provider-product
uri: http://127.0.0.1:7001
predicates:
- After=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]
4.4.2 Before Route Predicate Factory
接收一个日期参数,判断请求日期是否早于指定日期,访问时间在2020-12-31 23:59:59之前所有请求都通过
predicates:
- Before=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]
4.4.3 Between Route Predicate Factory
接收两个日期参数,判断请求日期是否在指定时间段内,访问时间在2020-12-31 23:59:59至2022-12-31 23:59:59中间会通过
predicates:
- Between=2020-12-31T23:59:59.789+08:00[Asia/Shanghai],2022-12-31T23:59:59.789+08:00[Asia/Shanghai]
4.4.4 Cookie Route Predicate Factory
接收两个参数,Cookie 名字和一个正则表达式。
当只有一个Cookie name变量时,当输入带有Cookie name时就能通过,带有正则表达式的时候判断name和正则表达式是否匹配 成功才能通过
predicates:
- Cookie=chocolate, ch.p
4.4.5 Header Route Predicate Factory
接收两个参数,和Cookie一样,可以输入一个参数也可输入两个参数 判断Header是否与给定值匹配。
predicates:
- Header=X-Request-Id, \d+
4.4.6 Host Route Predicate Factory
接收一个参数,主机名模式。判断请求的Host是否满足匹配规则
predicates:
-Host=**.testhost.org
4.4.7 Method Route Predicate Factory
接收一个参数,判断请求类型是否跟指定的类型匹配,只有get或者post的请求才能通过
predicates:
- Method=GET,POST
4.4.8 Path Route Predicate Factory
接收一个参数,判断请求的URI部分是否满足路径规则,配合filter使用 例如
/api/product/test/1 转发到product时为 /product/test/1
predicates:
- Path=/api/**
filters:
- StripPrefix=1 # 转发之前 去掉1层路径
4.4.9 Query Route Predicate Factory
接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配
predicates:
- Query=nnname,abc. # .代表任意字符
4.4.10 The RemoteAddr Route Predicate Factory
接收一个IP地址段,判断请求主机地址是否在地址段中,不在指定地址中将访问失败
predicates:
- RemoteAddr=172.16.1.191/0 #0表示子网掩码
4.4.11 The Weight Route Predicate Factory
接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
product是两个副本 访问 http://localhost:9000/product/test/2 这样就每三次访问7002才访问一次7001
routes:
- id: provider-product
uri: http://127.0.0.1:7001
predicates:
- Weight=mygroup, 1
- id: provider-product2
uri: http://127.0.0.1:7002
predicates:
- Weight=mygroup, 3
4.4.12 自定义断言
所有断言都是 AbstractRoutePredicateFactory 接口所有也可以自定义一个断言实现这个接口
实现一个自定义断言,当满足一个年龄范围内才能通过
package com.rem.cloudalibaba.predicate;
import com.alibaba.nacos.common.utils.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 自定义断言工厂
*
* @author Rem
* @date 2021-09-23
*/
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
/**
* 判断传入的age 是否在设置的断言内
*
* @param config
* @return
*/
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return exchange -> {
//从exchange获取传入的参数
String ageStr = exchange.getRequest().getQueryParams().getFirst("age");
if (StringUtils.isNotEmpty(ageStr)) {
int age = Integer.parseInt(ageStr);
return age > config.getMinAge() && age < config.getMaxAge();
}
return false;
};
}
@Validated
public static class Config {
private Integer minAge;
private Integer maxAge;
public Integer getMinAge() {
return minAge;
}
public void setMinAge(Integer minAge) {
this.minAge = minAge;
}
public Integer getMaxAge() {
return maxAge;
}
public void setMaxAge(Integer maxAge) {
this.maxAge = maxAge;
}
}
}
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Age=18,60 # 限制年龄只有在18到60岁之间的人能访问
访问 http://127.0.0.1:9000/product/test/1?**age=19** 就可以访问了 如果不带age就会出404
4.5 过滤
gateway 内部实现了很多过滤器
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
FallbackHeaders | 剔除响应头中重复的值 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个 preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host |
无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、 statusCode、denyEmptyKey、 emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始请求删除某个Header | Header名称 |
RewritePath | 为原始响应删除某个Header | 原始路径正则表达式以及重写后路径的正则表达式 |
RewriteResponseHeader | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改原始的请求路径修 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload TooLarge | 请求包大小,单位为字节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
常用的过滤器
4.5.1 路径过滤器 RewritePath
重写url,将 /api/product/test/1 重写为 product/test/1 直接访问 http://127.0.0.1:9000/api/product/test/1即可
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/api/**
filters:
- RewritePath=/api(?<segment>/?.*), $\{segment}
4.5.2 路径过滤器 PrefixPath
直接访问 http://127.0.0.1:9000/test/1 可以在目标微服务前添加product 变成 /product/test/1
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/**
filters:
- PrefixPath=/product
4.5.3 路径过滤器 StripPrefix
去掉几层前缀就写多少 ,访问 http://127.0.0.1:9000/abc/product/test/1 去掉abc 这层前缀变成 /product/test/1
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/abc/**
filters:
- StripPrefix=1
4.5.4 路径过滤器 SetPath
重新修改url /abc/product/test/1 修改为 /product/test/1
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/abc/product/test/{segment}
filters:
- SetPath=/product/test/{segment}
4.5.5 参数过滤器 Parameter 参数过滤器
在请求的时候添加参数,就能自动带入到下个接口中去了
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/api/**
filters:
- StripPrefix=1
- AddRequestParameter=flag, 256
4.5.6 参数过滤器 Parameter 参数过滤器
正常访问 http://127.0.0.1:9000/api/product/test/2 接口 页面的HTTP Code就是405了
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/api/**
filters:
- StripPrefix=1
- SetStatus=405
4.5.7 自定义网关过滤器
自定义日志过滤器 继承 AbstractGatewayFilterFactory
package com.rem.cloudalibaba.filter;
import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* 自定义日志过滤器
*
* @author Rem
* @date 2021-09-22
*/
@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
/**
* 构造函数
*/
public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
/**
* 读取配置文件的参数,赋值到配置类中
*
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
/**
* 过滤器逻辑
*
* @param config
* @return
*/
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
if (config.isCacheLog()) {
System.out.println("cacheLog 已经开启了");
}
if (config.isConsoleLog()) {
System.out.println("console 已经开启了");
}
return chain.filter(exchange);
};
}
@Data
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/api/**
filters:
- StripPrefix=1
- Log=true, false
4.5.8 自定义全局过滤器
可以自定义实现自己的逻辑
访问 http://127.0.0.1:9000/api/product/test/1?**token=remTok **必须带Token 不然就鉴权失败
package com.rem.cloudalibaba.filter;
import com.alibaba.nacos.common.utils.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.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 全局过滤器
*
* @author Rem
* @date 2021-09-24
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
/**
* 完成判断逻辑
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token) || !"remTok".equals(token)) {
System.out.println("鉴权失败");
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//调用chain.filter继续向下游执行
return chain.filter(exchange);
}
/**
* 顺序,数值越小,优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
4.6 限流
4.6.1 计数器算法
以QPS(每秒查询率Queries-per-second)为100举例。从第一个请求开始计时。每个请求让计数器加一。当到达100以后,其他的请求都拒绝。
如果1秒钟内前200ms请求数量已经到达了100,后面800ms中500次请求都被拒绝了,这种情况称为“突刺现象”
4.6.2 限流算法
漏桶算法可以解决突刺现象。
和生活中漏桶一样,有一个水桶,下面有一个”漏眼”往出漏水,不管桶里有多少水,漏水的速率都是一样的。但是既然是一个桶,桶里装的水都是有上限的。当到达了上限新进来的水就装不了(主要出现在突然倒进来大量水的情况)。
4.6.3 令牌桶算法
令牌桶算法可以说是对漏桶算法的一种改进。
在桶中放令牌,请求获取令牌后才能继续执行。如果桶中没有令牌,请求可以选择进行等待或者直接拒绝。由于桶中令牌是按照一定速率放置的,所以可以一定程度解决突发访问。如果桶中令牌最多有100个,qps最大为100
4.6.3.1 基于令牌桶算法的URL限流
新增redis的poom 依赖
package com.rem.cloudalibaba.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* @author Rem
* @date 2021-10-20
*/
@Configuration
public class KeyResolveConfiguration {
@Bean
KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
namespace: 3d0a77b8-817f-499b-bfda-f90d5a6e4dab
server-addr: 192.168.19.128:13306
group: DEFAULT_GROUP
username: nacos
password: nacos
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/api/**
filters:
- StripPrefix=1
# 限流过滤器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒生成率
redis-rate-limiter.burstCapacity: 5 #令牌桶总容量 每秒最大请求量
redis-rate-limiter.requestedTokens: 1 # 每次请求消耗的令牌
key-resolver: "#{@pathKeyResolver}"
redis:
host: 192.168.19.128
当访问频率大于每秒5次并且还大于令牌桶生成令牌的速率, 就会报HHTP Code 429
4.6.3.2 基于令牌桶算法的参数限流
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("userId")));
}
4.6.3.3 基于令牌桶算法的IP限流
@Bean
KeyResolver ipKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()));
}
基于令牌的限流 KeyResolver在一个项目中只能存在一个 所以只能选择一种限流方式。
4.6.4 gateway整合 sentinel
配置后就可以直接通过sentinel 对接口限流
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
namespace: 3d0a77b8-817f-499b-bfda-f90d5a6e4dab
server-addr: 192.168.19.128:13306
group: DEFAULT_GROUP
username: nacos
password: nacos
sentinel:
# 服务启动直接建立心跳连接
eager: true
transport:
dashboard: 127.0.0.1:8080
port: 8719 #默认为8719 ,假如被占用会自动从8719开始依次+1扫描,直到未被占用的端口
datasource:
flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
groupId: SENTINEL_GROUP
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
dataId: ${spring.application.name}-flow-rules
rule-type: flow
gateway:
routes:
- id: provider-product
uri: lb://provider-product
predicates:
- Path=/api/**
filters:
- StripPrefix=1
# 限流过滤器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒生成率
redis-rate-limiter.burstCapacity: 5 #令牌桶总容量 每秒最大请求量
redis-rate-limiter.requestedTokens: 1 # 每次请求消耗的令牌
# key-resolver: "#{@userKeyResolver}"
redis:
host: 192.168.19.128