文章

微服务网关 Spring Cloud GateWay

微服务 Spring Cloud GateWay

概述

  • SpringCloud Gateway使用的是Webflux中的reactor-netty****响应式编程组件,底层使用了Netty通讯框架(非堵塞式IO)
  • Zuul1.x 是基于Servlet2.x 实现的使用的是堵塞式IO,很慢。
  • 作用:反向代理,鉴权,流量控制,熔断,服务监控。
  • gateway 的核心功能就是 路由规则(route),断言(predicate),过滤器(filter)
  • 架构图
    image-20200521154411829

gateway 的配置

  1. 依赖
    <!--gateway无需web和actuator,会出错-->
    <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>
    
  2. 配置文件
    # 主要就是配置 网关的路由(就是对请求的转发)
    server:
      port: 9527
    ​
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        fetch-registry: true
        register-with-eureka: true
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka/
    ​
    # 这种方式的缺点:不能实现负载均衡,因为转发的ip 写死了
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          routes:
            - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
              #匹配后提供服务的路由地址
              uri: http://localhost:8001
              predicates:
                - Path=/payment/get/** # 断言,路径相匹配的进行路由
            - id: payment_route2
              uri: http://localhost:8001
              predicates:
                Path=/payment/lb/** #断言,路径相匹配的进行路由
    
  3. 主启动类
    // 什么都没有,注册进 注册中心即可
    @SpringBootApplication
    @EnableEurekaClient
    public class GatewayMain9527 {
        public static void main(String[] args) {
            SpringApplication.run(GatewayMain9527.class, args);
        }
    }
    
  4. 使用方式
    • 原先我们通过微服务的ip 访问微服务,启动了gateway 之后,我们访问gateway,又gateway 转发到我们的微服务。
  5. 编码的方式配置路由规则
    /**
     * <p>
     * 编码的方式定制路由规则.挺麻烦的还不如直接在配置文件里面写
     */
    @Configuration
    public class GateWayConfig {
        /**
         * 配置了一个id为route-name的路由规则
         * 当访问localhost:9527/guonei的时候,将会转发至https://news.baidu.com/guonei
         *
         * @param routeLocatorBuilder
         * @return
         */
        @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
    ​
            RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
            routes.route("path_route_atguigu",
                    r -> r.path("/guonei")
                            .uri("https://news.baidu.com/guonei")).build();
    ​
            return routes.build();
        }
    ​
        @Bean
        public RouteLocator customRouteLocator2(RouteLocatorBuilder routeLocatorBuilder) {
            RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
            return routes.route("path_route_atguigu2", r -> r.path("/guoji").uri("https://news.baidu.com/guoji")).build();
        }
    }
    
  6. 其他的predicate (断言)
    predicates:
     - Path=/payment/get/** # 断言,路径相匹配的进行路由
     - After=2020-05-30T15:13:10.896+08:00[Asia/Shanghai] # 必须在这个时间之后才能范文(游戏开服)
     - Before=2017-01-20T17:42:47.789-07:00[America/Denver] # 必须在这个时间之前才能访问 (游戏停服)
     - Cookie=username,zzyy # cookie 校验
     - Header=X-Request-Id, \d+ #请求头要有X-Request-Id属性,并且值为正数
     - Host=**.atguigu.com
     - Method=GET
     - Query=username, \d+ # 要有参数名username并且值还要是正整数才能路由
    
  7. 过滤器
    # 还有很多过滤器,用到的时候自己找吧
    ​
    routes:
      - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
      predicates:
      - Path=/payment/get/** # 断言,路径相匹配的进行路由
      # 过滤,断言都成功之后会经过过滤器,我们可以在这里加东西
      filters:
      - AddRequestHeader=X-Request-red, blue
    
  8. 路由的时候使用负载均衡
    # 核心区别就是 cloud.gateway.discovery.locator.enabled: true
    # 还有 uri 写成 lb://微服务名
    ​
    cloud:
        gateway:
          discovery:
            locator:
              enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称j进行路由
          routes:
            - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
              # 匹配后提供服务的路由地址
              # uri: http://localhost:8001
              uri: lb://CLOUD-PAYMENT-SERVICE # 匹配提供服务的路由地址
              predicates:
                - Path=/payment/get/** # 断言,路径相匹配的进行路由
          
              # 过滤,断言都成功之后会经过过滤器,我们可以在这里加东西
              filters:
                - AddRequestHeader=X-Request-red, blue
            - id: payment_route2
              # uri: http://localhost:8001
              uri: lb://CLOUD-PAYMENT-SERVICE # 匹配提供服务的路由地址
              predicates:
                - Path=/payment/lb/** #断言,路径相匹配的进行路由
    
  9. 定义全局的过滤器
    /**
     * User: haitao
     * Date: 2020/5/30
     * <p>
     * 自定义 gateway 的全局过滤器,实现 GlobalFilter, Ordered 两个接口,然后注入IOC 容器中,
     * 就能发挥全局过滤器的功能
     */
    @Component // 添加到IOC 容器中
    @Slf4j
    public class MyLogGateWayFilter implements GlobalFilter, Ordered {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("***********come MyLogGateWayFilter in : " + new Date());
    ​
            String uname = exchange.getRequest().getQueryParams().getFirst("uname");
            if (uname == null) {
                log.info("*******用户名为null,非法用户");
                exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
                return exchange.getResponse().setComplete();
            }
            return chain.filter(exchange);
        }
    ​
        @Override
        public int getOrder() {
            return 0;
        }
    }
    
  10. curl 的高级使用
    # 不带任何参数
    curl http://localhost:9527/payment/lb
    # 携带cookie
    curl http://localhost:9527/payment/lb --cookie "username=spectrumrpc"
    # 携带请求头
    curl http://localhost:9527/payment/lb -H "X-Request-Id:123456"
    # host
    curl http://localhost:9527/payment/lb -H "Host: www.baidu.com"
    # 携带参数
    curl http://localhost:9527/payment/lb\?uname\=1
    

路由过滤

路由规则的定义语法如下:

spring:
  cloud:
    gateway:
      routes:
        - id: item
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**

其中routes对应的类型如下:

四个属性含义如下:

  • id:路由的唯一标示
  • predicates:路由断言,其实就是匹配条件
  • filters:路由过滤条件,后面讲
  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。

这里我们重点关注 predicates,也就是路由断言。SpringCloudGateway中支持的断言类型有很多:

名称 说明 示例
After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 是某两个时间点之前的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie 请求必须包含某些cookie - Cookie=chocolate, ch.p
Header 请求必须包含某些header - Header=X-Request-Id, \d+
Host 请求必须是访问某个host(域名) - Host=****.somehost.org,.anotherhost.org
Method 请求方式必须是指定方式 - Method=GET,POST
Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/**
Query 请求参数必须包含指定参数 **- Query=name, Jack或者- Query=name**
RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr=192.168.1.1/24
weight 权重处理

网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。

image-20240927090041437

如图所示:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为pre和post两部分,分别会在请求路由到微服务之前和之后被执行。
  4. 只有所有Filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filter的post逻辑。
  6. 最终把响应结果返回。

如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。

网关过滤器链中的过滤器有两种:

  • **GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route. **
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。

其实GatewayFilter和GlobalFilter这两种过滤器的方法签名完全一致:

/**
 * 处理请求并将其传递给下一个过滤器
 * @param exchange 当前请求的上下文,其中包含request、response等各种数据
 * @param chain 过滤器链,基于它向下传递请求
 * @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
 */
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。

Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route. 例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。 使用的使用只需要在application.yaml中这样配置:

spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: lb://test-service
        predicates:
          -Path=/test/**
        filters:
          - AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value

如果想要让过滤器作用于所有的路由,则可以这样配置:

spring:
  cloud:
    gateway:
      default-filters: # default-filters下的过滤器可以作用于所有路由
        - AddRequestHeader=key, value
      routes:
      - id: test_route
        uri: lb://test-service
        predicates:
          -Path=/test/**

自定义过滤器

无论是GatewayFilter还是GlobalFilter都支持自定义,只不过编码方式、使用方式略有差别。

自定义GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。最简单的方式是这样的:

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 获取请求
                ServerHttpRequest request = exchange.getRequest();
                // 编写过滤器逻辑
                System.out.println("过滤器执行了");
                // 放行
                return chain.filter(exchange);
            }
        };
    }
}

注意:该类的名称一定要以GatewayFilterFactory为后缀!

然后在yaml配置中这样使用:

spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器

另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:

​
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
                extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
​
    @Override
    public GatewayFilter apply(Config config) {
        // OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
        // - GatewayFilter:过滤器
        // - int order值:值越小,过滤器执行优先级越高
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 获取config值
                String a = config.getA();
                String b = config.getB();
                String c = config.getC();
                // 编写过滤器逻辑
                System.out.println("a = " + a);
                System.out.println("b = " + b);
                System.out.println("c = " + c);
                // 放行
                return chain.filter(exchange);
            }
        }, 100);
    }
​
    // 自定义配置属性,成员变量名称很重要,下面会用到
    @Data
    static class Config{
        private String a;
        private String b;
        private String c;
    }
    // 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("a", "b", "c");
    }
        // 返回当前配置类的类型,也就是内部的Config
    @Override
    public Class<Config> getConfigClass() {
        return Config.class;
    }
​
}

然后在yaml文件中使用:

spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。 还有一种用法,无需按照这个顺序,就是手动指定参数名:

spring:
  cloud:
    gateway:
      default-filters:
            - name: PrintAny
              args: # 手动指定参数名,无需按照参数顺序
                a: 1
                b: 2
                c: 3

自定义GlobalFilter

自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:

@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 编写过滤器逻辑
        System.out.println("未登录,无法访问");
        // 放行
        // return chain.filter(exchange);
​
        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.setRawStatusCode(401);
        return response.setComplete();
    }
​
    @Override
    public int getOrder() {
        // 过滤器执行顺序,值越小,优先级越高
        return 0;
    }
}
License:  CC BY 4.0