SpringCloud 12 —— 网关之Gateway (中) 过滤器

过滤器

目前GatewayFilter工厂的内建实现如下:

ID类名类型功能
StripPrefixStripPrefixGatewayFilterFactorypre移除请求URL路径的第一部分,例如原始请求路径是/order/query,处理后是/query
SetStatusSetStatusGatewayFilterFactorypost设置请求响应的状态码,会从org.springframework.http.HttpStatus中解析
SetResponseHeaderSetResponseHeaderGatewayFilterFactorypost设置(添加)请求响应的响应头
SetRequestHeaderSetRequestHeaderGatewayFilterFactorypre设置(添加)请求头
SetPathSetPathGatewayFilterFactorypre设置(覆盖)请求路径
SecureHeaderSecureHeadersGatewayFilterFactorypre设置安全相关的请求头,见SecureHeadersProperties
SaveSessionSaveSessionGatewayFilterFactorypre保存WebSession
RewriteResponseHeaderRewriteResponseHeaderGatewayFilterFactorypost重新响应头
RewritePathRewritePathGatewayFilterFactorypre重写请求路径
RetryRetryGatewayFilterFactorypre基于条件对请求进行重试
RequestSizeRequestSizeGatewayFilterFactorypre限制请求的大小,单位是byte,超过设定值返回413 Payload Too Large
RequestRateLimiterRequestRateLimiterGatewayFilterFactorypre限流
RequestHeaderToRequestUriRequestHeaderToRequestUriGatewayFilterFactorypre通过请求头的值改变请求URL
RemoveResponseHeaderRemoveResponseHeaderGatewayFilterFactorypost移除配置的响应头
RemoveRequestHeaderRemoveRequestHeaderGatewayFilterFactorypre移除配置的请求头
RedirectToRedirectToGatewayFilterFactorypre重定向,需要指定HTTP状态码和重定向URL
PreserveHostHeaderPreserveHostHeaderGatewayFilterFactorypre设置请求携带的属性preserveHostHeader为true
PrefixPathPrefixPathGatewayFilterFactorypre请求路径添加前置路径
HystrixHystrixGatewayFilterFactorypre整合Hystrix
FallbackHeadersFallbackHeadersGatewayFilterFactorypreHystrix执行如果命中降级逻辑允许通过请求头携带异常明细信息
AddResponseHeaderAddResponseHeaderGatewayFilterFactorypost添加响应头
AddRequestParameterAddRequestParameterGatewayFilterFactorypre添加请求参数,仅仅限于URL的Query参数
AddRequestHeaderAddRequestHeaderGatewayFilterFactorypre添加请求头

这些filter的执行顺序和配置定义的顺序是有关系的

过滤器 MapRequestHeader

MapRequestHeader GatewayFilter 工厂采用两个参数:fromHeader 和 toHeader.

应用配置

这里我们继续使用上一节的项目模块。配置中设置谓词的After 类型,如果不设置谓词会报错,我们MapRequestHeader的两个参数分别设置值,最终第一个参数的值会拼在第二个参数后面。如果需要继续请求,那么在后面的服务接收到的Header值就会是新拼接的数据值。添加配置:

---
spring:
  cloud:
    gateway:
      routes:
        - id: map_request_header_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - MapRequestHeader=Blue, X-Request-Red
  profiles: map_request_header_route

将profiles.active改为map_request_header_route

说明: fromHeader 和 toHeader 在上例中分别为 Blue 和 X-Request-Red .

在ribbon-server 服务端添加接收方法

    /**
     * 配合gateway
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/header")
    public String header(@RequestHeader(value = "Host", required = false)  String host,
                         @RequestHeader(value = "Blue", required = false)  String blue,
                         @RequestHeader(value = "X-Request-Red", required = false)  String requestRed) {

        StringBuilder sp=new StringBuilder();
        if(StrUtil.isNotBlank(host)){
            sp.append("Host:").append(host).append(System.lineSeparator());
        }
        if(StrUtil.isNotBlank(host)){
            sp.append("Blue:").append(blue).append(System.lineSeparator());
        }
        if(StrUtil.isNotBlank(host)){
            sp.append("X-Request-Red:").append(requestRed).append(System.lineSeparator());
        }
        return sp.toString();
    }

启动应用访问127.0.0.1:8699/user/header测试

过滤器 StripPrefix

StripPrefix GatewayFilter工厂采用一个参数:part, 这个参数在请求转发到下游之前去除路径的前part部分

StripPrefix可以接受一个非负整数,对应的具体实现是StripPrefixGatewayFilterFactory,从名字就可以看出它的作用是去掉前缀的,那个整数即对应层数。

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: strippath_route
          uri: lb://ribbon-server
          predicates:
            - Path=/gateway/**
          filters:
            - StripPrefix=1 #去掉Path的第一部分也就是/gateway
  profiles: strippath_route

启动测试,我们通过 Spring Cloud Gateway 访问/gateway/user/1,那么当网关服务向后转发请求时,会去掉/gateway

过滤器 Prefix

该PrefixPath、GatewayFilter工厂采用单个prefix参数

添加配置

下面配置中,我们设置了一个路由前缀,会在我们输入的路径前面拼上我们设置的前缀,比如/1将向发送请求/user/1。

---
spring:
  cloud:
    gateway:
      routes:
        - id: prefixpath_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - PrefixPath= /user #前缀路由,所有请求会加上前缀/user
  profiles: prefixpath_route

访问127.0.0.1:8699/1进行测试

PrefixPathGatewayFilterFactory源码分析

刚开始初始化的时候会设置网关是否已设置前缀gatewayAlreadyPrefixed为false,如果已经设置,就会以设置的为主。
修改gatewayAlreadyPrefixed的状态为true,并修改url的地址加上config.prefix中的值,config.prefix的值可以图片。
修改完之后重新构建ServerHttpRequest,之后交给NettyRoutingFilter执行

@Override
public GatewayFilter apply(Config config) {
   return new GatewayFilter() {
      @Override
      public Mono<Void> filter(ServerWebExchange exchange,
            GatewayFilterChain chain) {
         boolean alreadyPrefixed = exchange
               .getAttributeOrDefault(GATEWAY_ALREADY_PREFIXED_ATTR, false);
         if (alreadyPrefixed) {
            return chain.filter(exchange);
         }
         exchange.getAttributes().put(GATEWAY_ALREADY_PREFIXED_ATTR, true);

         ServerHttpRequest req = exchange.getRequest();
         addOriginalRequestUrl(exchange, req.getURI());
         String newPath = config.prefix + req.getURI().getRawPath();

         ServerHttpRequest request = req.mutate().path(newPath).build();

         exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());

         if (log.isTraceEnabled()) {
            log.trace("Prefixed URI with: " + config.prefix + " -> "
                  + request.getURI());
         }

         return chain.filter(exchange.mutate().request(request).build());
      }

      @Override
      public String toString() {
         return filterToStringCreator(PrefixPathGatewayFilterFactory.this)
               .append("prefix", config.getPrefix()).toString();
      }
   };
}

过滤器 PreserveHostHeader

PreserveHostHeader GatewayFilter Factory没有参数。 此过滤器设置路由过滤器将检查的请求属性,以确定是否应发送原始主机头,而不是http客户端确定的主机头

添加配置

spring:
  cloud:
    gateway:
      routes:
        - id: preserve_host_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - PreserveHostHeader
  profiles: preserve_host_route

PreserveHostHeaderGatewayFilterFactory 源码

这个filter就往exchange添加PRESERVE_HOST_HEADER_ATTRIBUTE,设置为true

public class PreserveHostHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory {

   public GatewayFilter apply() {
      return apply(o -> {
      });
   }

   public GatewayFilter apply(Object config) {
      return new GatewayFilter() {
         @Override
         public Mono<Void> filter(ServerWebExchange exchange,
               GatewayFilterChain chain) {
            exchange.getAttributes().put(PRESERVE_HOST_HEADER_ATTRIBUTE, true);
            return chain.filter(exchange);
         }

         @Override
         public String toString() {
            return filterToStringCreator(PreserveHostHeaderGatewayFilterFactory.this)
                  .toString();
         }
      };
   }

}

NettyRoutingFilter 源码

NettyRoutingFilter 是最后真正发起http请求的。

@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

   String scheme = requestUrl.getScheme();
   if (isAlreadyRouted(exchange)
         || (!"http".equals(scheme) && !"https".equals(scheme))) {
      return chain.filter(exchange);
   }
   setAlreadyRouted(exchange);

   ServerHttpRequest request = exchange.getRequest();

   final HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
   final String url = requestUrl.toASCIIString();

   HttpHeaders filtered = filterRequest(getHeadersFilters(), exchange);

   final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
   filtered.forEach(httpHeaders::set);

   boolean preserveHost = exchange
         .getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);

   Flux<HttpClientResponse> responseFlux = this.httpClient.headers(headers -> {
      headers.add(httpHeaders);
      if (preserveHost) {
         String host = request.getHeaders().getFirst(HttpHeaders.HOST);
         headers.add(HttpHeaders.HOST, host);
      }
      else {
         // let Netty set it based on hostname
         headers.remove(HttpHeaders.HOST);
      }
   }).request(method).uri(url).send((req, nettyOutbound) -> {
      if (log.isTraceEnabled()) {
         nettyOutbound.withConnection(connection -> log.trace(
               "outbound route: " + connection.channel().id().asShortText()
                     + ", inbound: " + exchange.getLogPrefix()));
      }
      return nettyOutbound.send(request.getBody()
            .map(dataBuffer -> ((NettyDataBuffer) dataBuffer).getNativeBuffer()));
   }).responseConnection((res, connection) -> {

      // Defer committing the response until all route filters have run
      // Put client response as ServerWebExchange attribute and write
      // response later NettyWriteResponseFilter
      exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
      exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);

      ServerHttpResponse response = exchange.getResponse();
      // put headers and status so filters can modify the response
      HttpHeaders headers = new HttpHeaders();

      res.responseHeaders()
            .forEach(entry -> headers.add(entry.getKey(), entry.getValue()));

      String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
      if (StringUtils.hasLength(contentTypeValue)) {
         exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR,
               contentTypeValue);
      }

      HttpStatus status = HttpStatus.resolve(res.status().code());
      if (status != null) {
         response.setStatusCode(status);
      }
      else if (response instanceof AbstractServerHttpResponse) {
         // https://jira.spring.io/browse/SPR-16748
         ((AbstractServerHttpResponse) response)
               .setStatusCodeValue(res.status().code());
      }
      else {
         // TODO: log warning here, not throw error?
         throw new IllegalStateException("Unable to set status code on response: "
               + res.status().code() + ", " + response.getClass());
      }

      // make sure headers filters run after setting status so it is
      // available in response
      HttpHeaders filteredResponseHeaders = HttpHeadersFilter
            .filter(getHeadersFilters(), headers, exchange, Type.RESPONSE);

      if (!filteredResponseHeaders.containsKey(HttpHeaders.TRANSFER_ENCODING)
            && filteredResponseHeaders.containsKey(HttpHeaders.CONTENT_LENGTH)) {
         // It is not valid to have both the transfer-encoding header and
         // the content-length header.
         // Remove the transfer-encoding header in the response if the
         // content-length header is present.
         response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
      }

      exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES,
            filteredResponseHeaders.keySet());

      response.getHeaders().putAll(filteredResponseHeaders);

      return Mono.just(res);
   });

   if (properties.getResponseTimeout() != null) {
      responseFlux = responseFlux.timeout(properties.getResponseTimeout(),
            Mono.error(new TimeoutException("Response took longer than timeout: "
                  + properties.getResponseTimeout())))
            .onErrorMap(TimeoutException.class,
                  th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT,
                        th.getMessage(), th));
   }

   return responseFlux.then(chain.filter(exchange));
}

不过,配置了不是有http 确认的Host ,可能会使网关报400错误

我们把配置- PreserveHostHeader去掉,发现默认是false,然后会把header 中的Host 取消掉,转发到服务的时候Header 就是网关服务的了。

过滤器 RedirectTo

RedirectTo GatewayFilter工厂有两个参数,status和url。该status参数应该是300系列重定向HTTP代码,例如301。该url参数应该是有效的URL。这是Location标题的值。以下清单配置了一个RedirectTo GatewayFilter

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: redirect_to_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - RedirectTo=302, http://www.163.com
  profiles: redirect_to_route

启动测试,上面配置,当我们访问http://127.0.0.1:8699/redirectTo路径时,并不会映射到http://localhost:8774/redirectTo.而是被转发到了http://www.163.com

过滤器 RemoveHopByHopHeadersFIlter

RemoveHopByHopHeadersFilter GatewayFilter工厂从转发的请求中删除header,被删除的header列表来自IETF。

默认删除的header是:

  • Connection
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade

如果希望去修改它,可以通过设置spring.cloud.gateway.filter.remove-non-proxy-headers.headers属性来移除header列表。

RemoveRequestHeaderGatewayFilter工厂需要一个name参数。它是要删除的标题的名称

添加配置

我们在配置中设置需要删除key为Blue的Header值,实际请求中会将Header中的Blue的值。

---
spring:
  cloud:
    gateway:
      routes:
        - id: remove_request_header_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - RemoveRequestHeader=Blue #移除Header中的Blue
  profiles: remove_request_header_route

启动项目访问127.0.0.1:8699/user/header测试

可以看到虽然设置了Header中Blue的值,但是被清掉了

过滤器 RemoveResponseHeader

在返回客户端之前,删除指定响应头的数据,RemoveResponseHeader GatewayFilter 工厂需要一个name参数

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: remove_response_header_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - RemoveResponseHeader=Content-Type #移除response的header中的Content-Type
  profiles: remove_response_header_route

启动,访问,测试

可以看到返回消息中Content-Type被移除了

过滤器 RemoveRequestParameter

该RemoveRequestParameter``GatewayFilter工厂需要一个name参数。它是要删除的查询参数的名称

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: remove_response_parameter_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - RemoveRequestParameter=ids
  profiles: remove_response_parameter_route

启动,访问,测试

因为请求参数ids被移除,所以抛出异常

过滤器 RewritePath

该RewritePath GatewayFilter工厂采用的路径regexp参数和replacement参数。这使用Java正则表达式提供了一种灵活的方式来重写请求路径。

---
spring:
  cloud:
    gateway:
      routes:
        - id: rewritepath_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - RewritePath=/gateway/(?<segment>.*), /$\{segment}
  profiles: rewritepath_route

启动,访问127.0.0.1:8699/gateway/user/1测试,会被定位到127.0.0.1:8699/user/1

过滤器 RewriteLocationResponseHeader

RewriteLocationResponseHeader GatewayFilter 工厂 用来重写 响应头的Location 的值,以摆脱后端特定的详细信息。这需要 stripVersionMode , locationHeaderName , hostValue 和 protocolsRegex 参数。

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: rewritelocationresponseheader_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,
  profiles: rewritelocationresponseheader_route

示例:对于请求api.example.com/some/object/name Location 响应头的值object-service.prod.example.net/v2/some/object/id将会被重写为api.example.com/some/object/id

参数 stripVersionMode 可选值如下:

  • NEVER_STRIP:版本信息不会被剥离,即使原始请求路径不包含版本
  • AS_IN_REQUEST:仅当原始请求路径不包含任何版本时,才会剥离版本【默认】
  • ALWAYS_STRIP:即使原始请求路径包含版本,也会剥离版本

参数 hostValue,如果提供,会替换 Response Header Location 值中的host:port 部分;如果不提供,则会使用 Request 的 Host 作为默认值

参数 protocolRegex,协议会与该值匹配,如果不匹配,过滤器不回做任何操作,默认值http|https|ftp|ftps

过滤器 SetPath

该SetPathGatewayFilter工厂采用的路径template参数。通过允许路径的模板段,它提供了一种操作请求路径的简单方法。这使用了Spring Framework中的URI模板。允许多个匹配段

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: setpath_route
          uri: lb://ribbon-server
          predicates:
            - Path=/{segment}
          filters:
            - SetPath=/user/{segment}
  profiles: setpath_route

启动,测试可以看到127.0.0.1:8699/1实际走的是127.0.0.1:8699/user/1

过滤器 SetRequestHeader

该SetRequestHeader GatewayFilter工厂采用name和value参数。会把外部设置的同名name的值替换掉。

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: setrequestheader_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - SetRequestHeader=X-Request-Red, 54321
  profiles: setrequestheader_route

启动,测试。

image.png

可以看到X-Request-Red传入12345,但是变成了54321

对于SetResponseHeader返回的header修改也是一样,如:

---
spring:
  cloud:
    gateway:
      routes:
        - id: setrequestheader_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - SetResponseHeader=X-Request-Red, fwcloud
  profiles: setrequestheader_route

过滤器 SetStatus

该SetStatus GatewayFilter工厂采用单个参数,status。它必须是有效的SpringHttpStatus。它可以是整数值404或枚举的字符串表示形式:NOT_FOUND。

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: setstatus_route
          uri: lb://ribbon-server
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - SetStatus= 404 #或者 NOT_FOUND
  profiles: setstatus_route

启动访问127.0.0.1:8699/user/1
可以看到访问任何接口,不管成功还是失败返回的状态码都是404

过滤器 RequestSize

当请求大小高于允许的限制大小时,RequestSize GatewayFilter工厂将会阻止转发到下游服务。过滤器将RequestSize 参数作为请求的允许大小限制(以字节为单位)

添加配置

---
spring:
  cloud:
    gateway:
      routes:
        - id: request_size_route
          uri: lb://cloud-upload
          predicates:
            - After=2020-05-05T13:20:14.455+08:00[Asia/Shanghai]
          filters:
            - name: RequestSize
              args:
                maxSize: 5000000
  profiles: request_size_route

启动我们之前的上传服务,再启动项目。

我们随便上传一个大一点的文件。

过滤器 Default

如果想添加一个过滤器去应用在所有的路由上,可以使用spring.cloud.gateway.default-filters来配置,这个属性接收一个Filter列表。

添加配置

这里的配置跟过滤器 StripPrefix类似,只不过把filters的配置拿到了default-filters中,这样后面其它服务有类似的需求就可以省不少配置信息,比如设置前缀。

---
spring:
  cloud:
    gateway:
      default-filters:
        - StripPrefix=1
      routes:
        - id: strippath_route
          uri: lb://ribbon-server
          predicates:
            - Path=/gateway/**
  profiles: default_path_route

启动,访问,测试

过滤器 Java Bean

前面我们说的都是基于application.yml(.properties)的形式,Spring Cloud Gateway 还支持Java Bean 的配置方式。

将过滤器开关和profile 注释掉

server:
  port: 8699
spring:
  application:
    name: gateways-gateway
#  profiles:
#    active: default_path_route
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/

Java Bean 配置

    /**
     * Java 的流式 API 进行路由的定义
     * @param builder
     * @return
     */
    @Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/gateway/**")
                        .filters(f -> f.stripPrefix(1)
                                .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                        .uri("lb://ribbon-server")
                        .order(0)
                        .id("strippath_route")
                )
                .build();
    }

启动,访问测试,可以看到和应用配置效果一样。

更新时间:2020-05-06 14:09:26

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud12网关之gateway中
最后更新:2020-05-06 14:09:26

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×