Gateway服务网关

1 什么是Spring Cloud Gateway
Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filter链的方式提供了网关基本的功能。目前最新版Spring Cloud中引用的还是Zuul1.x版本,而这个版本是基于过滤器的,是阻塞IO,不支持长连接。

Zuul2.x版本一直跳票,2019年5月,Netflix终于开源了支持异步调用模式的Zuul2.0版本,真可谓千呼万唤始出来。但是Spring Cloud已经不再集成Zuul2.x了,那么是时候了解-下Spring Cloud Gateway了。
Spring Cloud Gateway是基于Spring生态系统之上构建的API网关,包括:Spring5、Spring Boot2和Project Reactor。SpringCloud Gateway旨在提供一种简单而有效的方法来路由到APl,并为它们提供跨领域的关注点,例如:安全性,监视/指标,限流等。由于Spring5.0支持Netty、Http2,而Spring Boot2.0支持Spring5.0,因此Spring Cloud Gateway支持Netty和Http2顺理成章。
2 什么是服务网关
API Gateway(APIGW/API网关),顾名思义,是出现在系统边界上的一个面向API的、串行集中式的强管控服务,这里的边界是企业IT系统的边界,可以理解为`企业级应用防火墙`,主要起到`隔离外部访问与内部系统的作用`。在微服务概念的流行之前,API网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API网关的流行,源于近几年来移动应用与企业间互联需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一的Web应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。`随着微服务架构概念的提出,API网关成为了微服务架构的一个标配组件。`
API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供定制的API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。AP川网关并不是微服务场景中必须的组件,如下图,不管有没有API网关,后端微服务都可以通过API很好地支持客户端的访问。

但对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入API网关也有一系列的好处:
- 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
- 聚合后台服务,节省流量,提高性能,提升户体验
- 提供安全、流控、过滤、缓存、计费、监控等API管理功能
3 为什么要使用网关
- 单体应用:浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对于单体应用来说是不需要网关的。
- 微服务:微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/软件工具)想要请求对应的服务,都需要知道机器的具体IP或者域名URL,当微服务实例众多时,这是非常难以记忆的,对于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识解析判断出具体的微服务地址,再把请求转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。

总结:
如果让客户端直接与各个微服务交互:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 身份认证问题,每个微服务需要独立身份认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务
- 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难
因此,我们需要网关介于客户端与服务器之间的中间层,所有外部请求率先经过微服务网关,客户端只需要与网关交互,只需要知道风网关地址即可。这样便简化了开发且有以下优点:
- 易于监控,可在微服务网关收集监控数据并将其推送到外部系统进行分析
- 易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,从而无需在每个微服务中进行认证
- 减少了客户端与各个微服务之间的交互次数
4 网关解决了什么问题

网关具有身份认证与安全、审查与监控、动态路由、负载均衡、缓存、请求分片与管理、静态响应处理等功能。当然最主要的职责还是与“外界联系”。
总结一下,网关应当具备以下功能:
- 性能:AP1高可用,负载均衡,容错机制。
- 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
5 常用网关解决方案
5.1 Nginx + Lua
Nginx是由IgorSysoev为俄罗斯访问量第二的Rambler..ru站点开发的,一个高性能的HTTP和反向代理服务器。Ngnix一方面可以做反向代理,另外一方面做可以做静态资源服务器。
Nginx适合做门户网关,是作为整个全局的网关,对外的处于最外层的那种;而Gateway属于业务网关,主要用来对应不同的客户端提供服务,用于聚合业务。各个微服务独立部臀,职责单一,对外提供服务的时候需要有一个东西把业务聚合起来。
Gateway可以实现熔断、重试等功能,这是Nginx不具备的
5.2 Kong
Kong是Mashape提供的款API管理软件,它本身是基于Ngnix+Lua的,但比Nginx提供了更简单的配置方式,数据采用了ApacheCassandra/PostgreSQL存储,并且提供了一些优秀的插件,比如验证,日志,调用频次限制等。Kong非常诱人的地方就是提供了大量的插件来扩展应用,通过设置不同的插件可以为服务提供各种增强的功能。
优点:基于Nginx所以在性能和稳定性上都没有问题。Kong作为一款商业软件,在Ngix上做了很扩展工作,而且还有很多付费的商业插件。Kog本身也有付费的企业版,其中包括技术支持、使用培训川服务以及API分析插件。
缺点:如果你使用Spring Cloud,Kong如何结合目前已有的服务治理体系?
5.3 Traefik
Traefik是一个开源的Go语言开发的为了让部署微服务更加便捷而诞生的现代HTTP反向代理、负载均衡工具。它支持多种后台(Docker、Swarm、Kubernetes、Marathon、Mesos、Consul、Etcd、Zookeeper、BoltDB、Rest API、file)来自动化、动态的应用它的配置文件设置。Traefik拥有一个基于AngularJS编写的简单网站界面,支持Rest API,配置文件热更新,无需重启进程。高可用集群模式等。
相对Spring Cloud和Kubernetes而言,目前比较适合Kubernetes。
5.4 SpringCloud Netflix Zuul
Zuul是Netflix公司开源的一个API网关组件,Spring Cloud对其进行二次基于Spring Boot的注解式封装做到开箱即用。目前来说,结合Sring Cloud提供的服务治理体系,可以做到请求转发,根据配置或者默认的路由规则进行路由和Load Balance,无缝集成Hystrix。
虽然可以通过自定义Filter实现我们想要的功能,但是由于Zuul本身的设计是基于
单线程的接收请求和转发处理,是阻塞IO,不支持长连接。目前来看Zuul就显得很鸡肋,随着Zuul2.x一直跳票(2019年5月发布了Zuul2.0版本),Spring Cloud 推出自己的Spring Cloud Gateway。大意就是:Zuul已死,Spring Cloud Gateway永生(手动狗头)。
Zuul 1.0

Zuul 2.0

5.5 SpringCloud Gateway
本文主角
6 环境准备
eureka-server:注册中心eureka-server02:注册中心provider-service:商品服务,提供了根据主键查询商品接口http://localhost:7070/product/{id}order-servic:订单服务,提供了根据主键查询订单接口http://localhost:9090/order/{id}且订单服务调用商品服务。
7 Nginx实现API网关

之前的课程中我们已经详细的讲解过Nginx关于反向代理、负载均衡等功能的使用,这里不再赘述。这里主要通过Nginx来实现API网关方便大家更好的学习和理解Spring Cloud Gateway的使用。
7.1 下载
官网:http://nginx.org/en/download.html下载稳定版。为了方便学习,请下载Windows版本。

8.2 安装
解压文件后直接运行根路径下的nginx.exe文件即可。
Nginx默认端口为80,访问:http://localhost:80/看到下图说明安装成功

8.3 配置路由规则
进入Nginx的conf目录,打开nginx.conf文件,配置路由规则:
http {
...
server {
listen 80;
server_name localhost;
...
# 路由到商品服务
location /api-product {
proxy_pass http://localhost:8909/;
}
# 路由到订单服务
location /api-order {
proxy_pass http://localhost:9021/;
}
...
}
...
}
8.4 访问
之前我们如果要访问服务,必须由客户端指定具体服务地址访问,现在统一访问Ngiⅸ,由Ngix实现网关功能将请求路由至具体的服务。
访问:http://localhost/api-product/product/1结果如下:

访问:http://localhost/api-order/order/1结果如下:

8 Gateway实现API网关
官网文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/
8.1 核心概念
**路由(Route)**:路由是网关最基础的部分,路由信息由ID、目标URI、一组断言和一组过滤器组成。如果断言路由为真,则说明请求的URI和配置匹配。
**断言(Predicate)**:Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的
ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于Http Request中的任何信息,比如请求头和参数等。
**过滤器(Filter)**:一个标准的Spring Web Filter。Spring Cloud Gateway中的Filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器将会对清求和响应进行处理。
8.2 工作原理

如上图所示,客户端向`Spring Cloud Gateway`发出请求。再由网关处理程序`Gateway Handler Mapping`映射确定与请求相匹配的路由,将其发送到网关Web处理程序`Gateway Web Handler`。该处理程序通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器由虚线分隔的原因是,过滤器可以在发送代理清求之前和之后运行逻辑。所有`pre`过滤器逻辑均被执行。然后发出代理请求。发出代理请求后,将运行`post`过滤器逻辑。
8.3 搭建网关服务
8.3.1 创建项目
创建Maven项目。
8.3.2 添加依赖
<!--spring cloud gateway 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意
因为SpringCloud Gateway用的是响应式编程,需要netty的环境,所以不能够添加传统的
spring-boot-starter-web依赖。
8.3.3 配置文件
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
8.3.4 启动类
package top.cluski;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}
8.4 配置路由规则
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应的URL的请求,将匹配到的请求追加到目标URI之后
- 请求
http://localhost:3001/product/1将会路由至http://localhost:8909/proudct/1
访问http://localhost:3001/product/1结果如下:

9 路由规则
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。
- Spring Cloud Gateway包含许多内置的Route Predicate Factories。
- 所有这些断言都匹配HTTP请求的不同属性。
- 多个Route Predicate Factories可以通过逻辑与(and)结合起来一起使用。
路由断言工厂RoutePredicateFactory包,含的主要实现类如图所示,包括Datetime、请求的远端地址、路由权重、请求头、Host地址、请求方法、请求路径和请求参数等类型的路由断言。

9.1 Path
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应的URL的请求,将匹配到的请求追加到目标URI之后
请求http://localhost:3001/product/1将会路由至http://localhost:8909/proudct/1
9.2 Query
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
# - Query=token # 匹配请求参数中包含token请求
- Query=token, abc. # 匹配请求参数中包含token请求,并且其参数值满足正则表达式 abc. 的请求
-
Query=token:比如,http://localhost:3001/product/1?token=123 -
Query=token, abc.:比如,http://localhost:3001/product/1?token=abc1
9.3 Method
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Method=GET # 匹配任意的GET请求
9.4 DateTime
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
# 匹配中国上海时间 202-02-02 20:20:20 之后的请求
- After=2022-02-02T20:20:20.000+08:00[Asia/Shanghai]
9.5 RemoteAddr
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
- RemoteAddr=192.168.124.0/8 # 匹配远程地址请求是 192.168.124.0/8 的请求,8表示子网掩码
9.6 Header
spring:
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 唯一id
uri: http://localhost:8909/ # 目标URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Header=X-Request-Id, \d+ # 匹配请求头包含 X-Request-Id 并且其匹配正则表达式 \d+ 的请求
10 动态路由(服务发现的路由规则)
动态路由其实就是面向服务的路由,Spring Cloud Gateway支持与Nacos整合开发,根据serviceld自动从注册中心
地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用
路由配置。
10.1 动态获取URI2
10.1.1 配置文件
配置注册中心与路由规则
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
# 路由规则
discovery:
# 是否与服务发现组件进行结合,通过 serviceId 转发到具体的服务实例
enabled: true # 是否开启基于服务发现的路由规则
lower-case-service-id: true # 是否将服务名转成小写
11 过滤器
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
-
GatewayFilter:网关过滤器,需要通过spring.cloud.routes.filters配置在具体路由下,只作用在当前路由上或通过spring.c1loud.default-filters配置在全局,作用在所有路由上。 -
GlobalFi1ter:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务清求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
11.1 网关过滤器GatewayFilter
网关过滤器用于拦截并链式处理Web请求,可以实现横切与应用无关的需求,比如:安全、访问超时的设置等。修改传入的HTTP请求或传出HTTP响应。Spring Cloud Gateway包含许多内置的网关过滤器工厂一共有22个,包括头部过滤器、路径类过滤器、Hystⅸ过滤器和重写请求URL的过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途来划分,可以分为以下几f种:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter和Hystrix。

11.1.1 Path路径过滤器
Path路径过滤器可以实现URL重写,通过重写URL可以实现隐藏实际路径提高安全性,易于用户记忆和键入,易于被搜索引擎收录等优点。实现方式如下:
11.1.1.1.RewritePathGatewayFilterFactory
RewritePath网关过滤器工厂采用路径正则表达式参数和替换参数,使用Java正则表达式来灵活地重写请求路径。
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**, /api-gateway/**
filters:
# 将 /api-gateway/product/1 重写为 /product/1
- RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
访问http://localhost:3001/api-gateway/product/1,结果如下:
11.1.1.2 PrefixPathGatewayFilterFactory
PrefixPath网关过滤器工厂为匹配的URI添加指定的前缀。
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
# 匹配对应的URL请求,将匹配到的请求追加到目标URL之后
- Path=/**
filters:
# 将 /1 重写为 /product/1
- PrefixPath=/product
访问http://localhost:3001/1,结果如下:
11.1.1.3 StripPrefixGatewayFilterFactory
StripPrefix网关过滤器工厂采用一个参数StripPrefix,该参数表示在将请求发送到下游之前从请求中剥离的路径个数。
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
# 匹配对应的URL请求,将匹配到的请求追加到目标URL之后
- Path=/**
filters:
# 将 /api/123/product/1 重写为 /product/1
- StripPrefix=2
访问http://localhost:3001/api/123/product/1,结果如下:
11.1.1.4 SetPathGatewayFilterFactory
SetPath网关过滤器工厂采用路径模板参数。它提供了一种通过允许模板化路径段来操作请求路径的简单方法,使用了Spring Framework中的uri模板,允许多个匹配段。
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
# 匹配对应的URL请求,将匹配到的请求追加到目标URL之后
- Path=/api/product/{segment}
filters:
# 将 /api/product/1 重写为 /product/1
- SetPath=/product/{segment
访问http://localhost:3001/api/product/1,结果如下:
11.1.2 Parameter参数过滤器
AddRequestParameter 网关过滤器工厂会将指定参数添加至匹配的下游请求中。
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
# 匹配对应的URL请求,将匹配到的请求追加到目标URL之后
- Path=/api-gateway/**
filters:
# 将 /api/product/1 重写为 /product/1
- RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
- AddRequestParameter=flag, 1
修改商品服务的控制层代码:
@RestController
@RequestMapping("/product")
public class ProductController {
@Resource
private ProductService productService;
// @GetMapping("/{id}")
@RequestMapping("/{id}")
public Product selectProduct(@PathVariable Integer id, String flag) {
System.out.println("flag = " + flag);
return productService.selectProductById(id);
}
}
访问http://localhost:3001/api-gateway/product/1,控制台会输出以下结果:
flag = 1
11.1.3 Status状态过滤器
SetStatus网关过滤器工厂采用单个状态参数,它必须是有效的Spring HttpStatus。它可以是整数404或枚举NOT_FOUND的字符串表示。
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
# 匹配对应的URL请求,将匹配到的请求追加到目标URL之后
- Path=/api-gateway/**
filters:
# 将 /api/product/1 重写为 /product/1
- RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
# 任何情况下,相应的 HTTP 状态都将设置为 404
- SetStatus=NOT_FOUND
访问http://localhost:3001/api/123/product/1,结果如下:
11.2 全局过滤器 GlobalFilter
全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成 GatewayFilterChain可识别的过滤器,它是请求业务以及路由的U!转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。

11.3 自定义过滤器
即使Spring Cloud Gateway自带许多实用的GatewayFilter Factory、Gateway Filter、Global Filter,但是在很多情景下我们仍然希望可以自定义自己的过滤器,实现一些强操作。
11.3.1 自定义网关过滤器
自定义网关过滤器需要实现以下两个接口:GatewayFilter、Ordered。
11.3.1.1 创建过滤器
package top.cluski.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定义网关过滤器
*/
public class CustomFilter implements GatewayFilter, Ordered {
/**
* 过滤器业务逻辑
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定义网关过滤器被执行");
return chain.filter(exchange); // 继续向下执行
}
/**
* 过滤器执行顺序,数值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
11.3.1.2 注册过滤器
package top.cluski.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.cluski.filter.CustomFilter;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route(r -> r
// 断言(判断条件)
.path("/product/**")
// 目标URI,路由到微服务的地址
.uri("lb://product-service")
// 注册自定义网关过滤器
.filter(new CustomFilter())
// 路由id,唯一
.id("product-service")
).build();
}
}
访问http://localhost:3001/product/1,控制台会输出自定义网关过滤器被执行。
11.3.2 自定义全局过滤器
自定义全局过滤器需要实现以下两个接口:GlobalFilter,ordered,通过全局过滤器可以实现权限校验,安全性验证等功能。
11.3.2.1 创建过滤器
实现指定接口,添加@Component注解即可。
CustomGlobalFilter.java
package top.cluski.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定义网关过滤器
*/
public class CustomGatewayFilter implements GatewayFilter, Ordered {
/**
* 过滤器业务逻辑
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定义网关过滤器被执行");
return chain.filter(exchange); // 继续向下执行
}
/**
* 过滤器执行顺序,数值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
11.3.2.2 访问
访问http://localhost:3001/product/1,控制台会输出自定义全局过滤器被执行。
11.3.3 统一鉴权
接下来我们在网关过滤器中通过token判断用户是否登录,完成一个统一鉴权案例。
11.3.3.1 创建过滤器
AccessFilter.java
package top.cluski.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
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;
import java.nio.charset.StandardCharsets;
/**
* 权限验证过滤器
*/
@Component
@Slf4j
public class AccessFilter implements GlobalFilter, Ordered {
/**
* 过滤器业务
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求参数
String token = exchange.getRequest().getQueryParams().getFirst("token");
// 业务逻辑
if (null == token) {
log.warn("token is null...");
ServerHttpResponse response = exchange.getResponse();
// 响应类型
response.getHeaders().add("Content-Type", "application/json; charset=utf- 8");
// 响应状态码, HTTP 401 错误代表用户没有访问权限
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 响应内容
String message = "{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}";
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
// 请求结束,不再继续向下请求
return response.writeWith(Mono.just(buffer));
}
log.info("token is ok");
return chain.filter(exchange);
}
/**
* 过滤器执行顺序
* @return
*/
@Override
public int getOrder() {
return 1;
}
}
11.3.3.2 访问
访问http://localhost:3001/product/1,结果:
{
"message": "Unauthorized"
}
访问http://localhost:3001/product/1?token=123123,结果:
{
"id": 1,
"productName": "冰箱",
"productNum": 1,
"productPrice": 2666.0
}
12 网关限流
顾名思义,限流就是限制流量,就像你宽带包有1个G的流量,用完了就没了。通过限流,我们可以很好地控制系统的QPS,从而达到保护系统的目的。
12.1 为什么需要限流
比如Web服务、对外API,这种类型的服务有以下几种可能导致机器被拖垮:
- 用户增长过快(好事)
- 因为某个热点事件(微博热搜)
- 竟争对象爬虫
- 恶意的请求
这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的。

从上图可以看出,对内而言:上游的A、B服务直接依赖了下游的基础服务C,对于A、B服务都依赖的基础服务C这种场景,服务A和B其实处于某种竞争关系,如果服务A的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务C并影响服务B,即雪崩效应。
12.2 限流算法
常见的限流算法有:
- 计数器算法
- 漏桶(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
12.2.1 计数器算法
计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于1分钟,重置counter重新计数,具体算法的示意图如下;
这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题,我们看下图:
从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求那么其实这个用户在1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
还有资源浪费的问题存在,我们的预期想法是希望100个清求可以均匀分散在这一分钟内,假设30s以内我们就请求上限了,那么剩余的半分钟服务器就会处于闲置状态,比如下图:
12.2.2. 漏桶算法
漏桶算法其实也很简单,可以粗路的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。

漏桶算法是使用队列机制实现的。
12.2.3 令牌桶算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。
场景大概是这样的:桶中一直有大量的可用令牌,这时进来的请求可以直接拿到令牌执行,比如设置QPS为100/s,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的100个清求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。
Spring Cloud Gateway内部使用的就是该算法,大概描述如下:
- 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
- 根据限流大小,设置按照一定的速率往桶里添加令牌:
- 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
- 请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除:
- 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,清求处理完之后将不会删除令牌,以此保证足够的限流。

漏桶算法主要用途在于保护他人,而令牌桶算法的主要目的在于保护自己,将请求的压力交由目标服务处理。架设突然进行很多请求,只有拿到令牌的这些请求会瞬时被处理调用目标服务。
12.3 Gateway限流
Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory过滤器工厂,使用Redis和Lua本实现了令牌桶的方式
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-redis-ratelimiter具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,Lua脚本在如下图所示的原码文件夹中:

12.3.1 添加依赖
<!--spirng data redis reactive 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!--commons-pool2 对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
12.3.2 限流规则
12.3.2.1 URI限流
配置文件:
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**
filters:
# 限流过滤器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@pathKeyResolver}" # 使用 Spel 表达式按名称引用Bean
redis:
timeout: 10000 # 连接超时时间
host: 192.168.124.29 # Redis服务器地址
port: 6379 # Redis服务端口
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 1024 # 最大链接数,默认8
max-wait: 10000 # 最佳链接阻塞等待时间,单位毫秒,默认-1
max-idle: 200 # 最大空闲连接,默认8
min-idle: 5 # 最小空闲连接,默认8
配置类:
package top.cluski.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;
/**
* 限流规则配置类
*/
@Configuration
public class KeyResolveConfig {
/**
* 限流规则
* @return
*/
@Bean
public KeyResolver pathKeyResolver() {
/*
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
};
*/
// JDK 1.8
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
多次访问:http://localhost:3001/product/1,结果如下

Redis结果如下:

12.3.2.2 参数限流
配置限流过滤器和限流过滤器引用的bean对象
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**
filters:
# 限流过滤器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@parameterKeyResolver}" # 使用 Spel 表达式按名称引用Bean
redis:
timeout: 10000 # 连接超时时间
host: 192.168.124.29 # Redis服务器地址
port: 6379 # Redis服务端口
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 1024 # 最大链接数,默认8
max-wait: 10000 # 最佳链接阻塞等待时间,单位毫秒,默认-1
max-idle: 200 # 最大空闲连接,默认8
min-idle: 5 # 最小空闲连接,默认8
配置类:
package top.cluski.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;
/**
* 限流规则配置类
*/
@Configuration
public class KeyResolveConfig {
/**
* 根据参数限流
* @return
*/
@Bean
public KeyResolver parameterKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
}
多次访问:http://localhost:3001/product/1,结果如下

Redis结果如下:

12.3.2.3 IP限流
配置限流过滤器和限流过滤器引用的bean
server:
port: 3001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.124.29:8848
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**
filters:
# 限流过滤器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@ipKeyResolver}" # 使用 Spel 表达式按名称引用Bean
redis:
timeout: 10000 # 连接超时时间
host: 192.168.124.29 # Redis服务器地址
port: 6379 # Redis服务端口
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 1024 # 最大链接数,默认8
max-wait: 10000 # 最佳链接阻塞等待时间,单位毫秒,默认-1
max-idle: 200 # 最大空闲连接,默认8
min-idle: 5 # 最小空闲连接,默认8
配置类:
package top.cluski.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;
/**
* 限流规则配置类
*/
@Configuration
public class KeyResolveConfig {
/**
* 根据ip限流
* @return
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
多次访问:http://localhost:3001/product/1,结果如下

Redis结果如下:

12.4 Sentinel限流
Sentinel支持对Spring Cloud Gateway、Netfilx Zuul等主流的API Gateway进行限流。

官方文档:
12.4.1 添加依赖
<!--sentinel 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
12.4.2 相关配置
application.yaml:
spring:
cloud:
sentinel:
filter:
enabled: false
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/product/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
discovery:
locator:
enabled: true
lower-case-service-id: true
配置类:
package top.cluski.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.Set;
@Configuration
public class GatewayConfig {
@PostConstruct
public void doInit() {
initGatewayRules();
}
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
/*
resource: 资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count: 限流阈值
intervalSec: 统计时间窗口,单位是秒,默认是1秒
*/
rules.add(new GatewayFlowRule("order-service")
.setCount(3) // 限流阈值
.setIntervalSec(60) // 统计时间窗口,单位是秒,默认是1秒
);
GatewayRuleManager.loadRules(rules);
}
}
13.4.3 访问
多次访问http://localhost:3001/order/1,结果会返回:
{
"code": 429,
"message": "Blocked by Sentinel: ParamFlowException"
}
接口BlockRequestHand1er的默认实现为DefaultBlockRequestHandler,当发限流时会返回默认的错误信息:Blocked by Sentinel:FlowException。我们可以通过GatewayCallbackManager定制异常提示信息。
13.4.4 自定义异常提示
GatewayCallbackManager的setBlockHand1er注册函数用于实现自定义的逻辑,处理被限流的请求。
package top.cluski.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.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
@Configuration
public class GatewayConfig {
@PostConstruct
public void doInit() {
initGatewayRules();
initBlockHandler();
}
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
/*
resource: 资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count: 限流阈值
intervalSec: 统计时间窗口,单位是秒,默认是1秒
*/
rules.add(new GatewayFlowRule("order-service")
.setCount(3) // 限流阈值
.setIntervalSec(60) // 统计时间窗口,单位是秒,默认是1秒
);
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流异常处理器
*/
private void initBlockHandler() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
HashMap<String, String> result = new HashMap<String, String>();
result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
result.put("route", "order-service");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
};
// 加载自定义限流异常处理器
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
多次访问http://localhost:3001/order/1,结果会返回:
{
"code": "429",
"message": "Too Many Requests",
"route": "order-service"
}
12.4.5 分组限流
package top.cluski.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
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.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
@Configuration
public class GatewayConfig {
@PostConstruct
public void doInit() {
initGatewayRules();
initBlockHandler();
initCustomizedApis();
}
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
/*
resource: 资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count: 限流阈值
intervalSec: 统计时间窗口,单位是秒,默认是1秒
*/
rules.add(new GatewayFlowRule("order-service")
.setCount(3) // 限流阈值
.setIntervalSec(60) // 统计时间窗口,单位是秒,默认是1秒
);
// 分组限流
rules.add(new GatewayFlowRule("product-api")
.setCount(3)
.setIntervalSec(60)
);
rules.add(new GatewayFlowRule("order-api")
.setCount(5)
.setIntervalSec(60)
);
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流异常处理器
*/
private void initBlockHandler() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
HashMap<String, String> result = new HashMap<String, String>();
result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
result.put("route", "order-service");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
};
// 加载自定义限流异常处理器
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
/**
* 限流分组
*/
private void initCustomizedApis() {
HashSet<ApiDefinition> definitions = new HashSet<>();
// product-api 组
ApiDefinition api1 = new ApiDefinition("product-api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem()
// 匹配 /product-service/product 以及其子路由的所有路径
.setPattern("/product-service/product/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
// order-api 组
ApiDefinition api2 = new ApiDefinition("order-api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem()
// 只匹配 /order-service/order/index
.setPattern("/order-service/order/index"));
}});
definitions.add(api1);
definitions.add(api2);
// 加载限流分组
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
13 高可用网关
业内通常用多少9来衡量网的可用性,例如QQ的可用性是4个9,就是说QQ能够保证在一年里,服务在99.99%的时间是可用的,只有0.01%的时间不可用,大约最多53分钟。
对于大多数网站,2个9是基本可用;3个9是叫高可用;4个9是拥有自动恢复能力的高可用。
实现高可用的主要手段是数据的冗余备份和服务的失效转移,这两种手段具体可以怎么做呢,在网关里如何体现?主要有以下几个方向:
- 集群部署
- 负载均衡
- 健康检查
- 节点自动重启
- 熔断
- 服务降级
- 接口重试
13.1 Nginx + 网关集群实现高可用网关

nginx配置文件:
# gateway集群
upstream gateway {
server 192.168.124.38:3001;
server 192.168.124.38:3002;
}
server {
listen 80;
server_name localhost;
# 转发配置
location / {
proxy_pass http://gateway;
}
}
13.2 总结
一个请求过来,首先经过Ngx的一层负载,到达网关,然后由网关负载到真实后端,若后端有问题,网关会进行重试访多次访问后仍返回失败,可以通过熔断或服务降级立即返回结果。而且,由于是负载均衡,网关重试时不一定会访问到出错的端。
至此Gateway服务网关所有的知识点就讲解结束了。非常感谢你可以认真学习至此,加油少年~
评论区