GateWay
1. 什么是网关
网关是微服务最边缘的服务,直接暴露给用户,用来做用户和微服务的桥梁
-
没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的ip:port,如果user-service并发比较大,则无法完成负载均衡
-
有网关:客户端访问网关,网关来访问微服务,这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现token拦截,权限验证,限流等操作
2. Spring Cloud Gateway简介
SpringCloud Gateway作为Spring Cloud生态的网关,目标是替代Zuul,在SpringCloud2.0以上的版本中,没有对新版本的zuul2.0以上的最新高性能版本进行集成,仍然还是使用的zuul1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于webFlux 框架实现的,而webFlux框架底层则使用了高性能的Reactor模式通信框架的Netty。
3. Spring Cloud Gateway工作流程
客户端向springcloud Gateway 发出请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
Handler 再通过指定的过滤器来将请求发送到我们实际的服务的业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送爱丽请求之前【pre】或之后【post】执行业务逻辑,对其进行加强或处理。
Filter在 【pre】 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等
在【post】 类型的过滤器中可以做响应内容、响应头的修改、日志的输出,流量监控等有着非常重要的作用。
总结:Gateway 的核心逻辑也就是 路由转发 + 执行过滤器链
4. Spring Cloud Gateway三大核心概念
4.1 Route(路由)
路由信息的组成:
由一个ID、一个目的URL、一组断言工厂、一组Filter组成。
如果路由断言为真,说明请求URL和配置路由匹配。
4.2 Predicate(断言)
Java 8中的断言函数。 lambda 四大接口 供给形,消费性,函数型,断言型
Spring Cloud Gateway中的断言函数输入类型是Spring 5.0框架中的ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于Http Request中的任何信息比如请求头和参数。
4.3 Filter(过滤) (重点)
一个标准的Spring WebFilter。
Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过
Gateway filter是针对某一个路由的filter 对某一个接口做限流
Global Filter个是针对全局的filter,例如token过滤,ip黑名单过滤等
5.Gateway入门
5.1新建项目gateway
注意:项目不要选择spring-web包,导入eureka-client和gateway
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5.2启动类
@SpringBootApplication
@EnableEurekaClient //网关也是eureka的客户端
public class Gateway80Application {
public static void main(String[] args) {
SpringApplication.run(Gateway80Application.class, args);
}
}
5.3修改配置文件
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
enabled: true #开启网关,默认是开启的
routes: #设置路由,注意是数组,可以设置多个,按照id做隔离
- id: user-service-router #路由id,没有要求,保持唯一即可
uri: http://localhost:8081 #设置真正的服务ip:port
predicates: #断言匹配
- Path=/info/** #和服务中的路径匹配,是正则匹配的模式
- id: provider-service-router
uri: http://localhost:8082
predicates:
- Path=/info/** #如果匹配到第一个路由,则第二个就不会走了,注意这不是负载均衡
#eureka的配置
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8761/eureka/
5.4启动测试
启动eureka,启动gateway,启动user-service, 此时可以通过80端口localhost/info访问用户服务
6.Gateway的路由配置方式
6.1代码注入方式
官网给出的配置类,我们照葫芦画瓢
https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#modifying-the-way-remote-addresses-are-resolved
6.1.1 创建配置类GatewayConfig
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes
.route("path_rote_guonei", r -> r.path("/guonei").uri("http://news.baidu.com/guonei"))
.route("path_rote_guoji", r -> r.path("/guoji").uri("http://news.baidu.com/guoji"))
.route("path_rote_tech", r -> r.path("/tech").uri("http://news.baidu.com/tech"))
.route("path_rote_lady", r -> r.path("/lady").uri("http://news.baidu.com/lady"))
.build();
return routes.build();
}
6.1.2测试
开发中最常用的还是在yml中进行配置
7. Gateway微服务名动态路由,负载均衡
7.1 概述
从之前的配置里面我们可以看到我们的URL都是写死的,这不符合我们微服务的要求,我们微服务是只要知道服务的名字,根据名字去找,而直接写死就没有负载均衡的效果了。
默认情况下Gateway会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。
需要注意的是uri的协议为lb(load Balance),表示启用Gateway的负载均衡功能。
lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri
协议:就是双方约定的一个接头暗号
7.2 最佳实践
7.2.1 修改gateway配置
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启动态路由
lower-case-service-id: true #动态路由小驼峰规则
routes: #设置路由,注意是数组,可以设置多个,按照id做隔离
- id: user-service-router #路由id,没有要求,保持唯一即可
uri: lb://provider #使用lb协议 微服务名称做负均衡
predicates: #断言匹配
- Path=/info/** #和服务中的路径匹配,是正则匹配的模式
- id: provider-service-router
uri: http://localhost:8082
predicates:
- Path=/info/** #如果匹配到第一个路由,则第二个就不会走了,注意这不是负载均衡
#eureka的配置
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8761/eureka/
7.2.2 启动测试
启动eureka-server,启动两个服务名为provider的服务,和uri里面lb://服务名一致
在provider里面提供两个接口/info
访问测试:http://localhost/info 正常访问
当我们新起一个服务,那么gateway可以实现服务发现功能,我们并没有在routers里面配置路由规则,然而我们访问 新起的provider-order-service,测试访问
http://localhost/provider-order-service/info 可以成功,这就是动态路由和服务发现
8.断言,Gateway里面有哪些断言
通俗的说,断言就是一些布尔表达式,满足条件的返回true,不满足的返回false。
Spring Cloud Gateway将路由作为Spring WebFlux HandlerMapping基础架构的一部分进行匹配。Spring Cloud Gateway包括许多内置的路由断言工厂。所有这些断言都与HTTP请求的不同属性匹配。您可以将多个路由断言可以组合使用
Spring Cloud Gateway创建对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。
8.1如何使用这些断言
使用断言判断时,我们常用yml配置文件的方式进行配置
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
enabled: true #开启网关,默认是开启的
routes: #设置路由,注意是数组,可以设置多个,按照id做隔离
- id: user-service #路由id,没有要求,保持唯一即可
uri: lb://provider #使用lb协议 微服务名称做负均衡
predicates: #断言匹配
- Path=/info/** #和服务中的路径匹配,是正则匹配的模式
- After=2022-07-20T17:42:47.789-07:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之后的请求,ZonedDateTime dateTime=ZonedDateTime.now()获得
- Before=2022-09-18T21:26:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之前的请求
- Between=2022-07-20T21:26:26.711+08:00[Asia/Shanghai],2022-09-20T21:32:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之间的请求
- Cookie=name,xiaobai #Cookie路由断言工厂接受两个参数,Cookie名称和regexp(一个Java正则表达式)。此断言匹配具有给定名称且其值与正则表达式匹配的cookie
- Header=token,123456 #头路由断言工厂接受两个参数,头名称和regexp(一个Java正则表达式)。此断言与具有给定名称的头匹配,该头的值与正则表达式匹配。
- Host=**.bai*.com:* #主机路由断言工厂接受一个参数:主机名模式列表。该模式是一个ant样式的模式。作为分隔符。此断言匹配与模式匹配的主机头
- Method=GET,POST #方法路由断言工厂接受一个方法参数,该参数是一个或多个参数:要匹配的HTTP方法
- Query=username #查询路由断言工厂接受两个参数:一个必需的param和一个可选的regexp(一个Java正则表达式)。
- RemoteAddr=192.168.1.1/24 #RemoteAddr路由断言工厂接受一个源列表(最小大小1),这些源是cidr符号(IPv4或IPv6)字符串,比如192.168.1.1/24(其中192.168.1.1是IP地址,24是子网掩码)。
8.2 断言总结
Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理
9. Filter过滤器工厂(重点)
9.1 概述
gateway里面的过滤器和Servlet里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http请求和返回Http响应,过滤器只能指定路由进行使用.
9.2 分类
9.2.1 按生命周期分两种
pre 在业务逻辑之前
post 在业务逻辑之后
9.2.2 按种类分也是两种
GatewayFilter 需要配置某个路由,才能过滤。如果需要使用全局路由,需要配置Default Filters。
GlobalFilter 全局过滤器,不需要配置路由,系统初始化作用到所有路由上。
全局过滤器 统计请求次数 限流 token的校验 ip黑名单拦截
9.2.3官网过滤器
官方过滤器中单一过滤器有31个,全局过滤器有9个。
https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories
9.3 自定义网关过滤器(重点)
9.3.1 自定义全局过滤器
全局过滤器的优点的初始化时默认挂到所有路由上,我们可以使用它来完成IP过滤,限流等功能
9.3.2 创建配置类GlobalFilterConfig
@Component
@Slf4j
public class GlobalFilterConfig implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("进入了我自己的全局过滤器");
String token=exchange.getRequest().getQueryParams().getFirst("token");
if(!StringUtils.hasText(token))
{
ServerHttpResponse response = exchange.getResponse();
ObjectMapper mapper=new ObjectMapper();
String str="没有权限不能访问";
try {
byte[] data = mapper.writeValueAsBytes(str);
DataBuffer buffer = response.bufferFactory().wrap(data);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
log.info("验证通过");
return chain.filter(exchange);
}
/**
* order越小 越先执行
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
9.3.1 访问测试
http://localhost/info
9.4 IP认证拦截实战
9.4.1创建IPGlobalFilter
@Component
@Slf4j
public class IPCheckFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String ip = exchange.getRequest().getHeaders().getHost().getHostName();
//这里写死了,只做演示
if (ip.equals("localhost")) {
//说明是黑名单里面的ip
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
Map<String, Object> map = new HashMap<>();
map.put("code", HttpStatus.UNAUTHORIZED);
map.put("msg", "非法访问");
response.getHeaders().add("content-Type", "application/json;charset=UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = objectMapper.writeValueAsBytes(map);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
}
/**
* 设置此过滤器的执行顺序
*
* @return
*/
@Override
public int getOrder() {
return 1;
}
}
9.4.2 测试访问
localhost/info?token=hello
显示非法访问
10. 限流实战
10.1 什么是限流
网关可以做很多的事情,比如,限流,当我们的系统被频繁的请求的时候,就有可能将系统压垮,所以为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中.
通俗的说,限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,限流大致分为两种:
1. IP限流(5s内同一个ip访问超过3次,则限制不让访问,过一段时间才可继续访问)
- 请求量限流(只要在一段时间内(窗口期),请求次数达到阀值,就直接拒绝后面来的访问了,过一段时间才可以继续访问)(粒度可以细化到一个api(url),一个服务)
10.2限流的算法–令牌桶
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流
10.3 Gateway结合redis实现请求量限流
Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory,我们可以直接使用。
目前RequestRateLimiterGatewayFilterFactory的实现依赖于Redis,所以我们还要引入spring-boot-starter-data-redis-reactive。
10.3.1修改Pom
<!--限流要引入Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
10.3.2定义KeyResolver
KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。
@Configuration
public class RequestRateLimiterConfig {
/**
* IP限流
* 把用户的IP作为限流的Key
*
* @return
*/
@Bean
@Primary // 相同类型的bean产生了,使用@Primary表示优先级高
public KeyResolver hostAddrKeyResolver() {
KeyResolver keyResolver=new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String str=exchange.getRequest().getRemoteAddress().getHostName();
System.out.println(str);
return Mono.just(str);
}
};
return keyResolver;
}
/**
* 请求接口限流
* 把请求的路径作为限流key
*
* @return
*/
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}
10.3.3修改配置文件
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
enabled: true
routes:
- id: user-service
uri: lb://provider
predicates:
- Path=/info/**
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@hostAddrKeyResolver}'
redis-rate-limiter.replenishRate: 1 #允许用户每秒处理多少个请求
redis-rate-limiter.burstCapacity: 3 #令牌桶的容量,允许在一秒钟内完成的最大请求数
redis:
host: localhost
port: 6379
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:9000/eureka/
在上面的配置文件,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该
过滤器需要配置三个参数:
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
10.3.4测试
http://localhost/info?token=hello 快速访问后报429
#令牌桶的容量,允许在一秒钟内完成的最大请求数
redis:
host: localhost
port: 6379
eureka:
instance:
instance-id:
s
p
r
i
n
g
.
a
p
p
l
i
c
a
t
i
o
n
.
n
a
m
e
:
{spring.application.name}:
spring.application.name:{server.port}
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:9000/eureka/
```properties
在上面的配置文件,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该
过滤器需要配置三个参数:
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
10.3.4测试
http://localhost/info?token=hello 快速访问后报429