Spring Cloud Gateway

Spring Cloud Gateway是Spring提供的API网关层,可以通过它代理请求,然后转发到真正的目标地址上。使用Spring Cloud Gateway需要加上spring-cloud-starter-gatewayspring-boot-starter-webflux依赖。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

如果应用中加了spring-cloud-starter-gateway依赖,但是又不希望启用Gateway时可以指定spring.cloud.gateway.enabled=false

Spring Cloud Gateway的自动配置由org.springframework.cloud.gateway.config.GatewayAutoConfiguration定义。现假设有一个服务对应的地址是http://localhost:8900,我们的网关希望请求路径是/hello/**时就路由到http://localhost:8900,那我们就可以进行如下这样的配置。假设我们的网关部署的端口是9103,当访问http://localhost:9103/hello/abc时就会被路由到http://localhost:8900/hello/abc

spring:
  cloud:
    gateway:
      routes:
        - id: route_id
          uri: http://localhost:8900
          predicates:
            - Path=/hello/**

定义网关的路由信息时通常需要定义三个信息,id、uri和predicates。id表示这一条路由规则的标识,可以随便定义;uri表示当这一路由定义匹配到了后请求需要转发到的目标uri;predicates用来定义路由匹配的条件,可以有多个。上面的配置定义的路由匹配的条件是请求路径满足/hello/**格式。匹配路由的条件(或者说是规则),除了通过请求路径进行匹配的Path外,还有很多,简单列举几个如下。

  • Before:当前时间在指定的时间前匹配,时间格式是ISO格式。Before=2019-01-20T17:42:47.789-07:00[America/Denver]表示按照美国Denver时间在2019年1月20日17:42以前匹配。
  • After:表示时间在指定的时间之后匹配。
  • Between:表示时间在指定的时间之间时匹配。如Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
  • Header:通过头信息匹配。Header=X-Request-Id, \d+表示头信息中包含名为X-Request-Id的头,且其值全部是数字组成(第二个参数表示值,且是正则表达式)。
  • Host:通过host匹配。Host=**.somehost.org表示当请求的头信息中拥有一个名为Host的头信息,且其值为foo.somehost.orgbar.somehost.org等时匹配。
  • Method:通过请求方法匹配。Method=GET表示当请求方法为GET请求时匹配。
  • Cookie:通过Cookie进行匹配。Cookie=chocolate, ch.p表示请求中含有一个名为chocolate值为匹配ch.p正则表达式的Cookie时匹配,所以这里值可以为chapchbp等。
  • Query:通过URL查询参数匹配。它接收两个参数,参数名和参数值,其中参数值是可选的,且是正则表达式。Query=foo, ba.表示请求中包含名为foo的查询参数且值为baa、bab等时匹配。
  • RemoteAddr:通过客户端IP地址来匹配。它可以同时匹配多个IP地址,多个IP时用逗号分隔,也可以按子网匹配。RemoteAddr=10.10.10.1/24,10.10.11.1表示客户端IP是10.10.10.*10.10.11.1时匹配。

可以在org.springframework.cloud.gateway.handler.predicate包下查看各个Predicate的定义,每个Predicate定义的类名称都是Predicate的名称加RoutePredicateFactory后缀,如Before Predicate的类名称是BeforeRoutePredicateFactory。

自定义路由匹配条件

自定义路由匹配条件可以新建一个名称以RoutePredicateFactory结尾的Class,该Class需要继承AbstractRoutePredicateFactory抽象类,并定义为bean。下面的代码中就定义了一个名为My的RoutePredicateFactory,其对应的配置类是其内部定义的Config,接收一个name参数,通过该参数可以指定Request请求中需要包含的参数名,当请求中包含该参数名且为GET请求时即表示满足某个路由的匹配条件。

@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {
  private static final String NAME_KEY = "name";
  public MyRoutePredicateFactory() {
    super(Config.class);
  }
  @Override
  public Predicate<ServerWebExchange> apply(Config config) {
    return exchange -> {
      ServerHttpRequest request = exchange.getRequest();
      if (HttpMethod.GET.equals(request.getMethod())
          && request.getQueryParams().containsKey(config.getName())) {
        return true;
      }
      return false;
    };
  }
  @Override
  public List<String> shortcutFieldOrder() {
    return Arrays.asList(NAME_KEY);
  }
  @Data
  public static class Config {
    private String name;
  }
}

假设现在有一个路由定义需要请求中包含参数abc且为GET请求即表示满足路由转发条件,则我们可以使用上面新定义的MyRoutePredicateFactory进行如下配置。

spring:
  cloud:
    gateway:
      routes:
        - id: route_id
          uri: http://localhost:8900
          predicates:
            - My=abc

GatewayFilter

Spring Cloud Gateway的路由定义除了可以定义路由的匹配条件外,还可以定义路由需要被处理的GatewayFilter。

GatewayFilter可以对路由的请求或响应内容进行更改。下面的代码就给这个路由定义了三个GatewayFilter,AddRequestHeader这个Filter给Header中加了名为ABC,值为DEF的Header,第二次又加了名为ABC2,值为DEF2的Header;AddRequestParameter这个Filter加了名为foo,值为bar的查询参数。

spring:
  cloud:
    gateway:
      routes:
        - id: route_id
          uri: http://localhost:8900
          predicates:
            - Path=/hello/**
          filters:
            - AddRequestHeader=ABC, DEF
            - AddRequestHeader=ABC2, DEF2
            - AddRequestParameter=foo, bar

如你所见,AddRequestHeader这类GatewayFilter一次只能添加一个Header,如果需要添加多个Header,则定义多个AddRequestHeader。

除了上面两个GatewayFilter外,Spring Cloud Gateway还内置了很多GatewayFilter。它们都定义在org.springframework.cloud.gateway.filter.factory包中,类名称都是XXXGatewayFilterFactory,那么对应的GatewayFilter的名称就是XXX,比如AddRequestHeaderGatewayFilterFactory就对应AddRequestHeader这个Filter,以下是其中部分的简要介绍。

  • AddResponseHeader:添加响应的Header。AddResponseHeader=X-Response-Foo, Bar表示添加名为X-Response-Foo,值为Bar的响应Header。
  • SetResponseHeader:设置响应的Header,它与AddResponseHeader的区别是它可能进行覆盖,而AddResponseHeader是完全追加的。
  • Hystrix:表示可以使用Hystrix进行封装,然后使用其断路器机制,使用该GatewayFilter时需要添加spring-cloud-starter-netflix-hystrix依赖。该Filter只是用来定义路由对应的HystrixCommand的名称,比如Hystrix=myCommandName表示当前路由对应的HystrixCommand的名称是myCommandName,然后就可以通过myCommandName来定义Hystrix的相关配置了,Hystrix的相关配置是定义在application.properties或application.yml中的,如hystrix.command.myCommandName.execution.isolation.thread.timeoutInMilliseconds=5000
  • PrefixPath:添加路径前缀。PrefixPath=/mypath表示路径自动添加前缀/mypath,所以当客户端访问的是/hello时转发给底层服务的路径是/mypath/hello
  • RemoveRequestHeader:从请求Header中移除某个Header。RemoveRequestHeader=X-Request-Foo表示将从请求Header中移除名为X-Request-Foo的Header,然后再转发到底层服务。
  • RemoveResponseHeader:从响应Header中移除某个Header。
  • RewritePath:重写路径采用的是正则表达式替换。如RewritePath=/foo/(?<segment>.*), /$\{segment}时,当访问的是/foo/abc转发到底层服务的是/abc
  • SetPath:它可以直接指定转发的Path,但通常会接收一个路径参数。比如下面这样,segment就是一个路径变量,当访问的是/foo/abc时将转发到/abc。Path中同时定义多个路径变量也是可以的。
spring:
  cloud:
    gateway:
      routes:
      - id: setpath_route
        uri: http://example.org
        predicates:
        - Path=/foo/{segment}
        filters:
        - SetPath=/{segment}
  • StripPrefix:定义转发前需要去除的前缀级数。StripPrefix=2表示需要去除两级,当访问/a/b/c/d时将转发到/c/d

关于GatewayFilter的更多介绍和配置信息请参考https://cloud.spring.io/spring-cloud-static/Finchley.SR2/multi/multi__gatewayfilter_factories.html。接下来介绍几个配置复杂一点的GatewayFilter。

关于Gateway可配置的更多信息请参考org.springframework.cloud.gateway.config.GatewayProperties

Hystrix GatewayFilter

使用Hystrix GatewayFilter的路由会被HystrixCommand封装起来。HystrixCommand配置的核心是commandName,Hystrix GatewayFilter就是用来指定其对应的commandName的,比如下面这样。

spring:
  cloud:
    gateway:
      routes:
      - id: hystrix_route
        uri: http://example.org
        filters:
        - Hystrix=myCommandName

然后我们就可以在application.properties或application.yml中定义Hystrix的相关配置了,比如hystrix.command.myCommandName.execution.isolation.thread.timeoutInMilliseconds=5000。使用Hystrix GatewayFilter时可以配置一个fallbackUri,其会在断路器打开时转为访问fallbackUri。fallbackUri必须以forward:开头,其对应的路径是Gateway应用的路径。比如下面的配置,当我们通过Hystrix设置了访问该路由的超时时间是5秒钟,超过5秒钟还没有返回时就将转为访问本Gateway应用的/hystrix/fallback

spring:
  cloud:
    gateway:
      routes:
        - id: hystrix_test
          uri: http://localhost:8900
          predicates:
            - Path=/hello/gateway/hystrix/{num}
          filters:
            - name: Hystrix
              args:
                name: myCommandName
                fallbackUri: forward:/hystrix/fallback

上面的配置,指定name和args属性是标准的org.springframework.cloud.gateway.filter.FilterDefinition的配置,直接写Hystrix=myCommandName是把它作为一个整体作为FilterDefinition的构造参数,其内部会再拆分为name和args。

要访问到上面的fallbackUri指定的forward:/hystrix/fallback我们可以在Gateway应用中定义如下Controller。

@RestController
@RequestMapping("hystrix/fallback")
public class HystrixFallbackController {
  @GetMapping
  public String fallback() {
    return "gateway fallback result";
  }
}

RequestRateLimiter GatewayFilter

RequestRateLimiter GatewayFilter使用org.springframework.cloud.gateway.filter.ratelimit.RateLimiter来限制一个用户的访问速度。Spring Cloud Gateway已经提供了一个基于Redis的实现,org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter。使用它需要添加spring-boot-starter-data-redis-reactive依赖。然后可以通过参数redis-rate-limiter.replenishRate来指定一个用户在每秒允许发起的最大请求数,多余的请求不会被丢弃;通过参数redis-rate-limiter.burstCapacity来设置一个用户每秒能发起的最大请求数,多余的请求会被丢弃。它俩的区别是前者相当于每秒能处理的请求数,多余的会留到下一秒处理,而后者是能够接受的最大的请求数,多余的会被丢弃。比如下面的配置设置了前者为10,后者为20,表示每秒能处理一个用户的10个请求,如果第一秒来了20个请求,那么剩余的10个将放到第二秒处理。

spring:
  cloud:
    gateway:
      routes:
        - id: requestratelimiter_route
          uri: http://example.org
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

如果把两者都设置为10,则第一秒来了20个请求时,将只能处理10个,另外10个将被丢弃。

限制的访问速度是根据org.springframework.cloud.gateway.filter.ratelimit.KeyResolver解析出来的Key来限制的,默认是基于Principal的实现,如果有需要可以实现自己的KeyResolver,然后把它定义为一个bean,通过SPEL表达式#{@myKeyResolver}来引用对应的bean。如果有需要也可以实现自己的RateLimiter,然后把它定义为一个bean,然后进行下面这样的配置。

spring:
  cloud:
    gateway:
      routes:
      - id: requestratelimiter_route
        uri: http://example.org
        filters:
        - name: RequestRateLimiter
          args:
            rate-limiter: "#{@myRateLimiter}"
            key-resolver: "#{@userKeyResolver}"

Retry GatewayFilter

Retry GatewayFilter可以对转发请求进行重试。可以通过retries、series、statuses、methods四个参数来控制重试机制。

  • retries用来指定最大的重试次数。
  • series用来指定需要重试的错误码系列,在没有指定状态码时使用。它的可选值可以参考org.springframework.http.HttpStatus.Series
  • statues用来指定需要重试的错误码。
  • methods用来指定允许重试的方法

它们的默认配置如下面代码所示。

private int retries = 3;
private List<Series> series = toList(Series.SERVER_ERROR);
private List<HttpStatus> statuses = new ArrayList<>();
private List<HttpMethod> methods = toList(HttpMethod.GET);

下面的配置就配置了该路由在遇到状态码为BAD_GATEWAY或INTERNAL_SERVER_ERROR时进行重试,最大重试次数为3,其它参数也可以进行类似的配置。

spring:
  cloud:
    gateway:
      routes:
        - id: retry_test
          uri: http://localhost:8900
          predicates:
            - Path=/hello/**
          filters:
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,INTERNAL_SERVER_ERROR

自定义GatewayFilter

自定义GatewayFilter可以选择实现GatewayFilterFactory,也可以是继承抽象类AbstractGatewayFilterFactory或AbstractNameValueGatewayFilterFactory等,它们都需要指定一个配置类,当然了,如果不需要指定额外的配置信息也可以不指定配置类,因为抽象类已经有默认的配置类了,AbstractNameValueGatewayFilterFactory默认的配置类是NameValueConfig。GatewayFilterFactory类的命名需要以GatewayFilterFactory结尾,对应的GatewayFilterFactory的名称就是去除GatewayFilterFactory之后的部分。比如下面我们定义的MyGatewayFilterFactory的名称就是My。然后需要把它定义为一个bean。下面的代码中我们继承了AbstractGatewayFilterFactory,然后通过构造方法指定了配置类为其内部的Config类,Config类定义了一个name属性。我们的处理逻辑就是在请求转发之前打印一句话,请求转发后打印一句话。

@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {
  private static final String NAME_KEY = "name";
  public MyGatewayFilterFactory() {
    super(Config.class);
  }
  @Override
  public GatewayFilter apply(Config config) {
    return (exchange, chain) -> {
      URI uri = exchange.getRequest().getURI();
      System.out.println("请求" + uri + "以前的处理,config配置的name为:" + config.getName());
      return chain.filter(exchange).then(Mono.fromRunnable(() -> {
        System.out.println("请求" + uri + "以后的处理,config配置的name为:" + config.getName());
      }));
    };
  }
  /**
   * 在配置该Filter时,采用简写形式(即FilterName=参数)时后面的参数对应Config类中参数的顺序。
   * @return
   */
  @Override
  public List<String> shortcutFieldOrder() {
    return Arrays.asList(NAME_KEY);
  }
  @Data
  public static class Config {
    private String name;
  }
}

接着我们就可以在需要使用MyGatewayFilterFactory的路由定义中进行配置了,比如下面的配置就使用了MyGatewayFilterFactory,并指定上面Config配置类的name属性值为ABCDE。以此种方式指定参数需要重写上面的shortcutFieldOrder()方法。

spring:
  cloud:
    gateway:
      routes:
        - id: route_id
          uri: http://localhost:8900
          predicates:
            - Path=/hello/**
          filters:
            - My=ABCDE

以下是等价的全配置,其通过name属性指定GatewayFilterFactory的名称,args指定对应配置类里面可定义的参数。

spring:
  cloud:
    gateway:
      routes:
        - id: route_id
          uri: http://localhost:8900
          predicates:
            - Path=/hello/**
          filters:
            - AddRequestHeader=ABC, DEF
            - AddRequestHeader=ABC1, DEF1
            - AddRequestParameter=foo, bar
            - AddRequestParameter=foo1, bar1
            - name: My
              args:
                name: ABCDE

GlobalFilter

GatewayFilter是针对于特定的路由作用的,而GlobalFilter是作用于所有的路由的。Spring Cloud Gateway已经内置了一些GlobalFilter,列举几个如下:

  • ForwardRoutingFilter 其会判断需要路由到的URI是否是forward开头的,如果是将把请求交由DispatcherHandler处理,即由当前Gateway应用自己处理。
  • LoadBalancerClientFilter 其会判断路由到的URI是否以lb开头的(如lb://servicename),如果是将使用LoadBalanceClient获取服务(前面的servicename)对应的一个真实的地址。
  • NettyRoutingFilter 当路由到的URI是以http或https开头的,会采用Netty的HttpClient处理路由地址。
  • GatewayMetricsFilter 负责收集某些指标信息。

LoadBalancerClientFilter

LoadBalancerClientFilter 其会判断路由到的URI是否以lb开头的(如lb://servicename),如果是将使用LoadBalanceClient获取服务(前面的servicename)对应的一个真实的地址。使用负载均衡需要Classpath下存在DiscoveryClient相关的实现,比如前面介绍的Eureka、Consul等。下面的代码中我们就定义了一个路由到的目标地址是依赖于LoadBalanceClient的,当我们访问/lb/a/b/c的时候,就将映射到lb_route,然后会从DiscoveryClient获取服务test的一个地址,假设获取到的地址是http://localhost:8080,那么请求会被转发到http://localhost:8080/a/b/c(StripPrefixGatewayFilter会把前缀/lb去掉)。

spring:
  cloud:
    gateway:
      routes:
        - id: lb_route
          uri:lb://test
          predicates:
            - Path=/lb/**
          filters:
            - StripPrefix=1

自定义GlobalFilter

有需要可以实现自己的GlobalFilter。自定义的GlobalFilter需要定义为bean,多个Filter之间可以通过@Order或Ordered接口指定顺序,数值越小的排在越前。

@Component
@Order(1)
public class MyGlobalFilter implements GlobalFilter {
  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    URI uri = exchange.getRequest().getURI();
    System.out.println("接收到请求:" + uri);
    return chain.filter(exchange).then(Mono.fromRunnable(() -> {
      System.out.println("请求处理结束:" + uri);
    }));
  }
}

当接收到某个请求时GatewayFilter和GlobalFilter可能是相互穿插的,它们会被放到同一个Filter链里面,对应的顺序将按照Spring的Order规范来排序。

通过Java定义路由

除了在配置文件中配置路由定义信息之外,也可以通过Java程序来配置路由信息。配置方式如下,在@Configuration类中定义一个返回值为RouteLocator的方法,方法可以接收一个RouteLocatorBuilder类型的参数,方法上需要加上@Bean。然后在方法体中通过RouteLocatorBuilder来定义路由信息。下面的代码就定义了一个接收请求/api/**,然后路由到http://localhost:8900的路由。route()方法可以调用多次,每次都将定义一个新的路由定义。

@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(r -> r.path("/api/**")
            .filters(f ->
                f.addResponseHeader("X-TestHeader", "foobar"))
            .uri("http://localhost:8900")
        ).build();
  }
}

https支持

Gateway应用也可以启用https支持,它的启用跟普通的Spring Boot应用的配置是一样的。比如下面代码中就定义了https对应的keyStore的路径为Classpath下的keystore.p12,keyStore的密码为Spring,key为Spring,keyStore的类型为PKCS12。

server:
  ssl:
    enabled: true
    key-alias: spring
    key-store-password: spring
    key-store: classpath:keystore.p12
    key-store-type: PKCS12

虽然Gateway应用启用了https协议,但是底层的服务还是可以使用http协议访问的,它们是相互独立的。如果我们需要路由到的底层的服务是需要通过https访问的,则可以配置spring.cloud.gateway.httpclient.ssl.useInsecureTrustManager=true来信任所有的证书,比如下面这样。

spring:
  cloud:
    gateway:
      httpclient:
        ssl:
          useInsecureTrustManager: true
      routes:
        - id: route_id
          uri: https://localhost:8900
          predicates:
            - Path=/hello/**
          filters:
            - AddRequestHeader=ABC, DEF
            - AddRequestParameter=foo, bar

Spring Cloud Gateway底层进行路由转发会使用Netty的reactor.ipc.netty.http.client.HttpClient,基于该HttpClient可配置的相关信息可参考org.springframework.cloud.gateway.config.HttpClientProperties

对于生产而言,信任所有的证书不是一个好的选择,它可能不安全,可通过如下方式配置你信任的公钥证书。

spring:
  cloud:
    gateway:
      httpclient:
        ssl:
          trustedX509Certificates:
          - cert1.pem
          - cert2.pem

跨域配置

Spring Cloud Gateway也支持跨域配置,相关可配置的信息可以参考org.springframework.cloud.gateway.config.GlobalCorsProperties。比如下面的配置就定义了所有Origin为test.elim.com的GET请求都允许访问当前的Gateway。

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "test.elim.com"
            allowedMethods:
            - GET

Actuator Endpoint支持

Spring Cloud Gateway也提供了基于Spring Boot Actuator的支持,提供了一个名为gateway的Endpoint。在项目中添加了spring-boot-starter-actuator依赖,并对外发布了gateway这个Endpoint后可以通过/actuator/gateway/routes查看所有的路由定义;可通过/actuator/gateway/routes/route_id查看id为route_id的路由定义;/actuator/gateway/routefilters可以查看可用的GatewayFilter定义;/actuator/gateway/globalfilters可查看所有的GlobalFilter定义;更多关于gateway Endpoint的操作可查看对应的源码org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint