Spring Cloud —— Gateway 服务网关

Posted 圣斗士Morty

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud —— Gateway 服务网关相关的知识,希望对你有一定的参考价值。

一、什么是服务网关

服务网关也叫 API 网关。

所谓 API 网关,就是只系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,如,认证、鉴权、监控、路由转发等等。其本身也是一个可以注册到 Nacos 上的微服务:

二、业界常见网关组件

  • nginx + lua脚本
    使用 Nginx 的反向代理和负载均衡可实现对 api 服务器的负载均衡和高可用,lua 是一种脚本语言,可以用来编写简单的逻辑,Nginx 支持 lua 脚本。
  • Kong
    基于 Nginx + lua 开发,性能高、稳定,有多个可用插件(限流、鉴权等),开箱即用。
    但支持 http 协议,二次开发限制较大,缺乏更易用的管控、配置方式。
  • Zuul
    Netflix 开源的网关,功能丰富,使用 Java 开发,易于二次开发。
    但缺乏管控,无法动态配置,依赖组件多,处理 http 请求需要依赖 web 容器,性能不如 Nginx。
  • Spring Cloud Gateway
    Spring 公司为了 替换 Zuul 而开发的网关组件。Spring Cloud 微服务 推荐使用的网关组件。

三、Spring Cloud Gateway

Spring Cloud Gateway 是 Spring 公司基于 Spring 5.0 ,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单、有效、统一的 API 路由管理方式。目标是取代 Netflix Zuul ,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供网关基本功能,例如,安全、监控和限流等。

优点:

  • 性能强劲:是第一代网关 Zuul 的1.6 倍
  • 功能强大:内置了很多实用功能,如转发、监控、限流等
  • 设计优雅,易扩展

缺点

  • 依赖 Netty 和 WebFlux,不是传统的 Servlet 编程模型,学习成本高
  • 不能将其部署在 Tomcat 、Jetty 等 Servlet 容器中,只能打 jar 包执行
  • Spring Boot 2.0 以上才支持

四、Gateway 快速入门

使用 Spring Cloud Gateway 实现最简单的请求路由。

4.1 创建 gateway 服务

首先创建一个全新的 api-gateway 服务。

4.2 添加 gateway 依赖和 nacos 依赖

注意,gateway 项目一定不要引入 web-starter 依赖,因为 Gateway 本身不是基于 Servlet 实现。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
@Slf4j
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class);
        
        log.info("-------------------启动成功--------------------");
    }
}

4.3 配置路由信息

先配置一些必须元素,如端口号,网关微服务的服务名称,以及 nacos 注册地址:

server:
  port: 7000
spring:
  application:
    name: shop-api-gateway
  nacos:
    discovery:
      server-addr: localhost:8848

然后,再配置路由信息:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # 企业应用中一般也要把网关服务注册到 nacos 上
    gateway:
      discovery:
        locator:
          enabled: true # 开启网关服务发现定位器,可以令 gateway 从nacos上定位具体的微服务地址
      routes: # 路由数组(路由就是指当请求满足什么样的条件的时候转发到哪台服务器上)
        - id: product_route # 当前路由的标识,要求唯一,默认UUID,
          # uri: http:// localhost:8081  # 请求最终要被转发的地址
          uri: lb://service-product  # 若使用 nacos 可以写微服务名称,lb->loadbalance调用服务
          order: 1  # 指的是路由的优先级,数字小优先级高
          predicates: # 断言,条件判断,返回值 boolean,转发请求要满足的条件
            - Path=/product-serv/**   # 当请求路径满足 Path 指定的规则时,此路由信息才会正常转发
          filters: # 过滤器,在请求传递过程中,对请求做一些手脚
            - StripPrefix=1  # 在请求转发之前去掉一层路径,

对于 routes 配置,如果想简单配置,可直接忽略,因为 Spring Cloud Gateway 提供了默认的路由规则,用户可以直接在网关ip:port 地址和具体请求路径之间加入微服务名称就可以实现路由转发,例如:

http://localhost:7000/service-product/product/1

但是企业级应用中,往往会处于灵活的考虑,配置 routes 选项。

4.4 测试路由转发

启动 shop-api-gateway 和 shop-product 微服务,测试路由功能

网关服务启动在 7000 端口上,服务也成功注册到 Nacos 上:


请求网关服务,获取商品信息:


可以看到,网关服务从 Nacos 上获取商品微服务的 ip 信息成功,并且路由成功转发,并返回了商品信息。

五、Gateway 执行流程

路由(route)是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义下面几个信息:

  • id ,路由标识,区别其他 route
  • uri , 路由指向的目的 uri,即客户端请求最终转发到的微服务
  • order,用于多个 route 之间的排序,数值越小匹配的优先级越高
  • predicate, 断言的作用是进行条件判断,只有断言都返回 true ,才能真正的执行路由
  • filter,过滤器用于修改请求和响应信息

路由的执行流程

  1. 客户端向 Gateway Server 发起请求
  2. 请求首先被 HttpWebHandlerAdapter 进行提取,组装成网关上下文
  3. 然后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用
  5. 若断言成功,由FilteringWebHandler 创建过滤器链并调用
  6. 请求会依次经过 PreFilter --> 微服务–> PostFilter 的方法,最终响应回客户端

六、Gateway 断言

Predicate 断言,用于进行条件判断,只有断言都返回真,才会真正的执行路由。
简单的说,断言就是用来判断在什么条件下才能进行路由转发

6.1 内置路由断言工厂

1、 基于 Datetime 类型的断言工厂
此类型断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定日期之间

-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]

2、基于远程地址的断言工厂
RemoteAddrRoutePredicateFactory: 接收一个IP 地址,判断请求主机地址是否在地址段中

-RemoteAddr=192.168.1.1/24

3、基于 Cookie 的断言工厂
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式,判断请求 cookie 是否具有给定名称且值与正则表达式匹配

-Cookie=chocolate, ch.

4、基于 Header 的断言工厂
HeaderRoutePredicateFactory :接收两个参数,标题名称和正则表达式。判断请求 header 中是否具有给定名称且值与正则表达式匹配。

-Header=X-Request-Id,\\d+

5、基于权重的断言工厂
WeightRoutePredicateFactory:接收一个[组名, 权重],然后对于同一个组内的路由按照权重转发,例如下面的例子中,有两个路由规则 weight_route1和weight_route2,根据 Weight 的配置信息,两个路由位于 同一group3中,根据权重,按照 1:9 的比例转发请求。

routes:
-id:weight_route1
uri:host1
predicates:
-Path=/product/**
-Weight=group3,1
-id:weight_route2
uri:host2
predicates:
-Path=/product/**
-Weight=group3,9

6、其他类型的断言工厂
篇幅有限,以下是 Gateway 内置的全部断言工厂,它们都继承自 AbstractRoutePredicateFactory,从命名就可以大体猜出它们的使用场景:

HeaderRoutePredicateFactory
PathRoutePredicateFactory
BeforeRoutePredicateFactory
CloudFoundryRouteServiceRoutePredicateFactory
QueryRoutePredicateFactory
RemoteAddrRoutePredicateFactory
MethodRoutePredicateFactory
CookieRoutePredicateFactory
WeightRoutePredicateFactory
AfterRoutePredicateFactory
BetweenRoutePredicateFactory
HostRoutePredicateFactory
ReadBodyPredicateFactory

6.2 自定义路由断言工厂

自定义一个路由断言,要求实现只允许年龄在 18 ~ 60 的请求访问。

1、声明一个断言

2、自定义断言工厂
创建一个专用的 predicates 包,用于存放各种断言工厂类,然后创建 AgeRoutePredicateFactory:

@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
    
    public AgeRoutePredicateFactory() {
        super(AgeRoutePredicateFactory.Config.class);
    }
    
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("minAge", "maxAge");
    }
    
    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
                if (!StringUtils.isEmpty(ageStr)) {
                    Integer age = Integer.valueOf(ageStr);
                    return age >= config.getMinAge() && age <= config.getMaxAge();
                }
                
                return false;
            }
        };
    }
    
    @Data
    @NoArgsConstructor
    public static class Config {
        private int minAge;
        private int maxAge;
    }
}

代码逻辑基本可以参考已有的 AbstractRoutePredicateFactory 的内置实现类,apply 方法是主要返回一个函数接口 Predicate,可以使用Lambda表达式, ServerWebExchange 是一个可以从中取出 请求Request 的对象,剩下的,就是进行 Request 中的各种判断逻辑了,上述代码示例中只是简单的从请求中取出 age 属性,判断年龄区间是否符合配置条件。

3、测试
启动 nacos,将 gateway 和 product 微服务都注册上去,然后分别尝试请求 不同的 age 属性:


只要在 18 ~ 60 之间的 age 属性,都可以请求成功,反之则路由拦截,说明我们的 断言生效了。

七、Gateway 过滤器

过滤器的作用,就是在请求的传递过程中,对请求和响应对象做一些手脚。
在 Gateway 中,过滤器的生命周期只有两个:pre 和 post。

  • pre:这种过滤器在请求被真正转发之前调用,可以利用这种过滤器实现身份验证、在集群中选择请求的微服务(负载均衡)、记录调试信息等。

  • post:这种过滤器在gateway 收到微服务的响应之后执行,这种过滤器可用来为响应添加标准的 http header、收集统计信息和指标、将响应从微服务发送到客户端等。

    Gateway 的 Filter 从作用范围可分为 GatewayFilter 和 GlobalFilter。

  • GatewayFilter :应用到单个路由或一个分组的路由上

  • GlobalFilter:应用到所有路由上

7.1 局部过滤器

7.1.1 内置局部过滤器

过滤器的用法和断言类似,所有的内置过滤器名称都遵循 :配置 + GatewayFilterFactory ,这样的格式。

内置局部过滤器有很多,都继承自 AbstractGatewayFilterFactory,以 SetStatusGatewayFilterFactory 为例,演示一下 SetStatus 过滤器的用法。

使用 SetStatus 可以修改响应码:

spring:
  cloud:
    gateway:
      routes:
        - id:
          uri: 
          ## 其他属性...
          filters: # 过滤器,在请求传递过程中,对请求做一些手脚
            - SetStatus=222 # 将响应码改为 222

7.1.2 自定义局部过滤器

1、添加一个日志级别过滤器
该过滤器并没有什么实质的意义,只是用于演示过滤器的自定义逻辑:

spring:
  cloud:
    gateway:
      routes:
        - id:
          uri: 
          ## 其他属性...
          filters: # 过滤器,在请求传递过程中,对请求做一些手脚
            - Log=true,false # Log=consoleLog,cacheLog 开启consoleLog,不开启cacheLog

2、编写过滤器工厂类

@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
    
    public LogGatewayFilterFactory() {
        super(LogGatewayFilterFactory.Config.class);
    }
    
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("consoleLog", "cacheLog");
    }
    
    public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if (config.isConsoleLog()) {
                    System.out.println("consoleLog 开启了");
                } else if (config.isCacheLog()) {
                    System.out.println("cacheLog 开启了");
                }
                // 将过滤器链传递下去
                return chain.filter(exchange);
            }
        };
    }
    
    @Data
    @NoArgsConstructor
    public static class Config {
        private boolean consoleLog;
        private boolean cacheLog;
    }
}

3、测试
请求网关路由接口,后台观察日志:

7.2 全局过滤器

全局过滤器作用于所有路由,无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

7.2.1 内置全局过滤器

7.2.2 自定义全局过滤器

自定义全局鉴权过滤器。

如上图所示,当客户端第一次访问的时候,网关服务会先去授权中心请求登录授权,并颁发token凭证,然后客户端每次访问微服务的时候,就携带 token 进行请求。那么在网关中,我们就可以自定义一个全局的鉴权过滤器,来完成第一次请求的鉴权工作。

这里简单实现鉴权的逻辑,颁发凭证等更细节的内容不做展开。

1、自定义全局过滤器类

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (!StringUtils.equals("admin", token)) {
            System.out.println("鉴权失败!");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        // 调用 chain.filter 继续向下执行
        return chain.filter(exchange);
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

2、测试鉴权过滤
全局过滤器无需配置,自动生效,我们写好 AuthGlobalFilter 之后,将其注册到 Spring 容器中即可。打开浏览器访问路由接口,发现,返回结果为 401:

若添加 token=admin,则可以访问成功:

八、网关限流

官方文档:网关限流

网关是所有请求的公共入口,所以可以在网关进行限流,本节采用 Sentinel 组件来实现网关限流。Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流网关进行限流。


从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源纬度的限流:

  1. route 纬度:即在Spring 配置文件中配置路由条目,资源名为对应的 routeId
  2. 自定义API纬度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组

8.1 route纬度的网关限流

1、在服务网关模块加入 sentinel 的网关适配依赖

 <dependency>
     <groupId>com.alibaba.csp</groupId>
     <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
 </dependency>

2、编写配置类
基于 Sentinel 的 Gateway 限流是通过其提供的 Filter 来完成的,使用时只需要注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。

@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 SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }
}

8.2 自定义 API 分组的网关限流

官方参考文档:网关限流

8.3 网关流控实现原理

当通过 GatewayRuleManager 加载网关流控规则(GatewayFlowRule)时,无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(ParamFlowRule),存储在 GatewayRuleManager 中,与正常的热点参数规则相隔离。转换时 Sentinel 会根据请求属性配置,为网关流控规则设置参数索引(idx),并同步到生成的热点参数规则中。

外部请求进入 API Gateway 时会经过 Sentinel 实现的 filter,其中会依次进行 路由/API 分组匹配、请求属性解析和参数组装。Sentinel 会根据配置的网关流控规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入 SphU.entry(res, args) 中。Sentinel API Gateway Adapter Common 模块向 Slot Chain 中添加了一个 GatewayFlowSlot,专门用来做网关规则的检查。GatewayFlowSlot 会从 GatewayRuleManager 中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会在参数最后一个位置置入预设的常量,达到普通流控的效果。

8.4 网关流控控制台

Sentinel 1.6.3 引入了网关流控控制台的支持,用户可以直接在 Sentinel 控制台上查看 API Gateway 实时的 route 和自定义 API 分组监控,管理网关规则和 API 分组配置。

在 API Gateway 端,用户只需要在原有启动参数的基础上添加如下启动参数即可标记应用为 API Gateway 类型:

# 注:通过 Spring Cloud Alibaba Sentinel 自动接入的 API Gateway 整合则无需此参数
-Dcsp.sentinel.app.type=1

添加正确的启动参数并有访问量后,我们就可以在 Sentinel 上面看到对应的 API Gateway 了。我们可以查看实时的 route 和自定义 API 分组的监控和调用信息,并针对其配置规则:

总结

api 服务网关提供统一的服务入口,通过路由、断言、过滤器,可以实现诸如负载均衡、路由判定、网关限流、鉴权等功能。

以上是关于Spring Cloud —— Gateway 服务网关的主要内容,如果未能解决你的问题,请参考以下文章

spring cloud gateway 如何工作

Spring Cloud Gateway集成

spring cloud gateway 的执行流程

spring cloud gateway 某些路由中跳过全局过滤器

spring cloud gateway 报错 Unable to find GatewayFilterFactory with name

Spring Cloud(18)——gateway