微服务实践(六)Spring Cloud Gateway

为什么需要网关

传统的单体架构中只有一个服务开放给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,那么作为客户端如何去调用这些微服务呢?如果没有网关的存在,只能在本地记录每个微服务的调用地址。

如果不使用网关,将会出现以下问题

  1. 复杂的路由转发:请求的路由转发逻辑将会在每个微服务内部实现,代码实现复杂,维护困难。
  2. 不可靠的负载均衡:每个微服务实例可能会采用不同的负载均衡策略,不能保证请求的可靠性和稳定性。
  3. 分散的服务聚合:服务聚合逻辑将分散在每个微服务内部实现,不能统一管理,带来更高的维护成本和风险。
  4. 安全性不高:如果每个微服务都实现身份验证和授权控制,将面临更高的安全风险。
  5. 无法统一的流量控制:如果每个微服务都实现流量控制,将面临更高的复杂度和不稳定性。

网关有哪些功能

网关是所有微服务的门户,路由转发仅仅是最基本的功能,除此之外还有其他的一些功能,比如:认证鉴权熔断限流日志监控等等。

网关的技术选型

Spring Cloud Gateway是一个基于Spring框架的API网关,它使用Spring WebFlux构建非阻塞API,提供动态路由、监控、限流、安全等特性。它支持多种方法的请求转发,包括WebSockets,同时还提供了自定义过滤器的接口,以便用户能够在请求和响应处理过程中添加自己的业务逻辑。

Spring Cloud Gateway 核心概念

Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念,如下表。

核心概念描述
Route(路由)网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(Filter)组成。
Predicate(断言)路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。
Filter(过滤器)过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。

注意:其中 Route 和 Predicate 必须同时声明。

路由Route

网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(GatewayFilter)组成。

spring:
  cloud:
    gateway:
      routes:
        - id: order-service  
          uri: http://localhost:8080
          predicates:
          ## 当请求的路径为order、orders开头的时,转发到http://localhost:8080服务上
            - Path=/order/**,/orders/**
  

断言工厂(Predicate Factory

Spring Cloud Gateway包含许多内置的路由断言工厂。这些断言都匹配HTTP请求的不同属性。多个路由断言工厂可以通过 and 组合使用。

断言示例说明
Path - Path=/order/** 当请求路径与 /order/** 匹配时,该请求才能被转发到对应uri配置的地址上
上。
Before- Before=2021-10-20T11:47:34.255+08:00[Asia/Shanghai]在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之前的请求,才会被转发到对应uri配置的地址上。
After- After=2021-10-20T11:47:34.255+08:00[Asia/Shanghai]在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之后的请求,才会被转发到对应uri配置的地址上。
Between- Between=2021-10-20T15:18:33.226+08:00[Asia/Shanghai],2021-10-20T15:23:33.226+08:00[Asia/Shanghai]在 2021 年 10 月 20 日 15 时 18 分 33.226 秒 到 2021 年 10 月 20 日 15 时 23 分 33.226 秒之间的请求,才会被转发到对应uri配置的地址上。
Cookie- Cookie=name,c.biancheng.net携带 Cookie 且 Cookie 的内容为 name=c.biancheng.net 的请求,才会被转发到对应uri配置的地址上。
Header- Header=X-Request-Id,\d+请求头上携带属性 X-Request-Id 且属性值为整数的请求,才会被转发到对应uri配置的地址上。
Method- Method=GET只有 GET 请求才会被转发到对应uri配置的地址上。

路由过滤器(GatewayFilter)

路由过滤器是针对指定路由进行的过滤器,可以使用Spring Cloud Gateway内置的路由过滤器,也可以使用自定义过滤器,以下截取官网前几个内置过滤器进行示例,更多内置过滤器请参考#gatewayfilter-factories

内置路由过滤器

名称示例说明
AddRequestHeader- AddRequestHeader=X-Request-Foo, Bar
对于所有匹配的请求,这将向下游请求的头中添加 x-request-foo:bar的header
RemoveRequestHeader- RemoveRequestHeader=X-Request-Foo这将在X-Request-Foo header被发送到下游之前删除它
AddResponseHeader- AddResponseHeader=X-Response-Foo, Bar对于所有匹配的请求,这会将x-response-foo:bar头添加到下游响应的header中
RemoveResponseHeader- RemoveResponseHeader=X-Response-Foo这将在返回到网关client之前从响应中删除x-response-foo头。
AddRequestParameter- AddRequestParameter=red, blue
对于所有匹配的请求,这将向下游请求添加red=blue查询字符串

自定义路由过滤器

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class CustomGatewayFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // your custom logic here
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // define the order of the filter in the chain
        return 0;
    }
}

使用路由过滤器

配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: route1
          uri: http://example.com
          filters:
            - name: CustomGatewayFilter

全局过滤器(GlobalFilter)

Spring Cloud Gateway具有全局过滤器的功能,全局过滤器是对所有路由生效的过滤器。可以使用全局过滤器来实现鉴权,限流,日志记录等功能。

示例代码:

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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.concurrent.atomic.AtomicLong;

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    private static final AtomicLong REQUEST_COUNTER = new AtomicLong(0);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 身份验证
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (token == null || !token.equals("valid-token")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // 请求校验
        if (!exchange.getRequest().getQueryParams().containsKey("key")) {
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }

        // 请求限流
        long requests = REQUEST_COUNTER.incrementAndGet();
        if (requests > 100) {
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

代码中实现了身份验证,请求校验和请求限流三个功能。对于身份验证,验证请求头中的Authorization字段是否存在且值为valid-token;对于请求校验,验证请求的参数中是否包含了key字段;对于请求限流,使用一个静态的AtomicLong变量记录请求数,超过100的请求则返回429状态码。

网关的搭建

搭建网关服务,首先我们需要创建一个Maven模块,建议选择一个空模块,否则可能会跟spring-boot-starter-web依赖冲突,造成无法启动。

创建完一个空的Maven项目之后,在pom.xml导入以下依赖

    <!--nacos服务发现依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2.2.0.RELEASE</version>
     <!--nacos配置中心依赖-->    
    </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
    <!--引入gateway 网关-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <version>2.2.0.RELEASE</version>
    </dependency>

注意:SpringCloud Gateway的版本跟Nacos的版本最好一致,与SpringBoot版本相兼容,以免出现意想不到的问题。可以参照下面的版本说明

版本说明

1. 新建GatewayApplication启动类。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}

2. 新建yml配置文件并添加以下配置:

server:
  port: 9000
spring:
  application:
    name: gateway #服务名称
  cloud:
    nacos:
      config:
        namespace: ${nacos.namespace}
        server-addr: ${nacos.server-addr}
        file-extension: yaml
      #服务发现配置
      discovery:
        server-addr: ${nacos.server-addr}
        namespace: ${nacos.namespace}
    gateway:
      routes:
        - id: order-service  # 路由id,自定义,只要唯一即可
          uri: lb://order-center-server  # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/order-server/** # 这个是按照路径匹配,只要以/order-server/开头就符合要求
          filters:
            - RewritePath=/order-server/(?<segment>.*), /$\{segment} #重写路径,将/order-server/{something}重写为/{something}
nacos:
  server-addr: 127.0.0.1:8848
  namespace:  a506f87b-88c7-4fb9-bea4-c59881e140e8

3. 启动GatewayApplication会自动将服务注册到Nacos上,接下来就可以使用lb://服务名代替服务地址了,它会自动从注册中心读取服务列表,如果有集群则会自动进行负载均衡。其实现原理是基于 The ReactiveLoadBalancerClientFilter(一个全局过滤器)进行实现。

4.启动order-center、gateway服务,可以看到已经将服务注册到了Nacos。这时候访问网关的域名会自动转发到对应路由的服务上去。

5.请求网关的order-sever服务,可以看到将请求成功进行了转发。