Spring Cloud Gateway

Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

Spring Cloud Gateway是基于Spring Boot 2.xSpring WebFluxProject Reactor 构建的。因此,在使用Spring Cloud Gateway时,许多不熟悉的同步库(例如,Spring Data和Spring Security)和模式可能不适用。如果您对这些项目不熟悉,建议您在使用Spring Cloud Gateway之前先阅读它们的文档以熟悉一些新概念。

Spring Cloud Gateway需要Spring Boot和Spring Webflux提供的Netty运行时。它不能在传统的Servlet容器中或作为WAR构建。

由于 SCG 是 netty+webflux 实现,webflux 与 web 是冲突的,所以不能引用依赖 spring-boot-starter-web ,否则提示冲突。

相关概念
  • Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
  • Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This allows developers to match on anything from the HTTP request, such as headers or parameters.
  • Filter: These are instances Spring Framework GatewayFilter constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request.

官方 demo 官方文档

本例子 GitHub

快速上手

1.引入依赖,版本:
  • Spring Cloud: Greenwich.RC2
  • Spring Boot: 2.1.1.RELEASE
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spring-cloud-gateway-sample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-cloud-gateway-sample</name>
    <description>Demo project for Spring Cloud Gateway</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RC2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
2.配置

定义路由断言,意味着匹配这个路径/**的请求都会转发到127.0.0.1:9090这个地址。

server:
  port: 8000
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
      - id: path_route2
        uri: http://127.0.0.1:9090
        order: -1
        predicates:
        - Path=/**

      discovery:
        locator:
          enabled: true
          lower-case-service-id: true

logging:
  level:
    org.springframework.cloud.gateway: TRACE
#    org.springframework.http.server.reactive: DEBUG
#    org.springframework.web.reactive: DEBUG
#    reactor.ipc.netty: DEBUG
#    reactor.netty: DEBUG

使用配置 yml 进行配置路由时,多个可以匹配的情况下。可以使用 order 进行优先级排序,这个参数同样适用于使用代码进行配置路由的情况。

其他断言配置方式参考官网文档

整合 Eureka 的默认路由

Zuul 默认会为所有服务都进行转发操作,我们只需要在访问路径上指定要访问的服务即可,通过这种方式就不用为每个服务都去配置转发规则,当新加了服务的时候,不用去配置路由规则和重启网关。

在 Spring Cloud Gateway 中当然也有这样的功能,通过配置即可开启,配置如下:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true

        routes:
        - id: myRoute
          uri: lb://service
          predicates:
          - Path=/service/**

依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Global Filter and GatewayFilter

GatewayFilter与GlobalFilter的区别

在一个高的角度来看,Global filters会被应用到所有的路由上,而Gateway filter将应用到单个路由上或者一个分组的路由上。在下面的案例中将会进行说明。

Global Filter

当请求进入(并与路由匹配)时,过滤Web处理程序会将的所有实例GlobalFilter和所有特定GatewayFilter于路由的实例添加到过滤器链中。该组合的过滤器链按org.springframework.core.Ordered接口排序,可以通过实现该getOrder()方法进行设置。

由于Spring Cloud Gateway区分了执行过滤器逻辑的“前”阶段和“后”阶段(请参阅:工作原理),因此优先级最高的过滤器将在“前”阶段中处于第一个阶段,而在“后”阶段中处于最后一个阶段

AuthFilter.java

@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("前执行");
        // String token = exchange.getRequest().getQueryParams().getFirst("authToken");
        String token = exchange.getRequest().getHeaders().getFirst("authToken");
        if (token == null || token.isEmpty()) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            // wrap内返回封装对象即可
            return response.writeWith(Mono.just(response.bufferFactory().wrap("无权限".getBytes())));
        }
       // 这样获取请求参数或则 RequestParamsFilter 这个
        Map<String, Object> data = exchange.getAttribute("cachedRequestBodyObject");
        return chain.filter(exchange).then(Mono.fromRunnable(() -> log.info("后执行")));
    }

    @Override
    public int getOrder() {
        return -100;
    }
}
  • 多个filter按照order顺序执行,越小(负值)执行越优先。post返回时则相反。执行最后一个前执行后再逆序执行后执行。
  • globalFilter 和 gatewayFilter 两条链不是同一个线程。线程参数 ThreadLocal 变量不能共享。包括 pre,post 都是两个线程。

配置注入

/**
 * @Description: 配置类的方式加载filter
 * @author: LinQin
 * @date: 2020/01/11
 */
@Configuration
@Slf4j
public class AutoFilter {
    @Bean
    public GlobalFilter authFilter(){
        return new AuthFilter();
    }
}

GatewayFilter

新建类和全局过滤器一致,接口换为GatewayFilter即可。其他一致。

RouteLocator绑定使用

 @Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(r -> r.path("/test/prefix/**")
               .filters(f -> f.stripPrefix(2)
                        .filter(new CustomFilter())
                        .addResponseHeader("X-Response-test", "test"))
               .uri("lb://SC-CONSUMER")
               .order(0)
               .id("test_consumer_service")
              )
        .build();
}

过滤器中获取请求参数

网上坑甚多,自己尝试了两个可用的。都是官方 issue 提供的。

1.重写ModifyRequestBodyGatewayFilterFactory

RequestParamsFilter.java

/**
 * 获取请求体  ModifyRequestBodyGatewayFilterFactory  预言类 ReadBodyPredicateFactory
 * @author 
 */
@Slf4j
public class RequestParamsFilter implements GlobalFilter, Ordered {

    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // ServerRequest serverRequest = new DefaultServerRequest(exchange);
        ServerRequest serverRequest = new DefaultServerRequest(exchange, messageReaders);
        // mediaType
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        // read & modify body
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                                                 .flatMap(body -> {
                                                     if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) {

                                                         // origin body map
                                                         Map<String, Object> bodyMap = decodeBody(body);

                                                         // decrypt & auth

                                                         // new body map
                                                         Map<String, Object> newBodyMap = new HashMap<>();

                                                         return Mono.just(encodeBody(newBodyMap));
                                                     }

                                                     if (MediaType.APPLICATION_JSON
                                                             .isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON_UTF8
                                                             .isCompatibleWith(mediaType)) {
                                                         // 获取请求体
                                                         log.info(body);
                                                     }
                                                     return Mono.empty();
                                                 });

        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());

        // the new content type will be computed by bodyInserter
        // and then set in the request decorator
        headers.remove(HttpHeaders.CONTENT_LENGTH);

        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
        return bodyInserter.insert(outputMessage,  new BodyInserterContext())
                           .then(Mono.defer(() -> {
                               ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                                       exchange.getRequest()) {

                                   public HttpHeaders getHeaders() {
                                       long contentLength = headers.getContentLength();
                                       HttpHeaders httpHeaders = new HttpHeaders();
                                       httpHeaders.putAll(super.getHeaders());
                                       if (contentLength > 0) {
                                           httpHeaders.setContentLength(contentLength);
                                       } else {
                                           httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                                       }
                                       return httpHeaders;
                                   }


                                   public Flux<DataBuffer> getBody() {
                                       return outputMessage.getBody();
                                   }
                               };
                               return chain.filter(exchange.mutate().request(decorator).build());
                           }));
    }


    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    private Map<String, Object> decodeBody(String body) {
        return Arrays.stream(body.split("&"))
                     .map(s -> s.split("="))
                     .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
    }

    private String encodeBody(Map<String, Object> map) {
        String collect = map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
                            .collect(Collectors.joining("&"));
        log.info(collect);
        return collect;
    }
}

这个是使用 yml 配置路由最好的获取请求参数的方法。适用版本Spring Cloud: Greenwich.RC2 、Spring Boot: 2.1.1.RELEASE其他版本可能存在有时不灵的情况。

2.使用预言类 ReadBodyPredicateFactory

直接使用代码路由配置方式使用

@Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        //@formatter:off
        return builder.routes()
                      /*
                    route1 是get请求,get请求使用readBody会报错
                    route2 是post请求,Content-Type是application/x-www-form-urlencoded,readbody为String.class
                    route3 是post请求,Content-Type是application/json,readbody为Object.class
                     */
                      .route("route1",
                              r -> r
                                      .method(HttpMethod.GET)
                                      .and()
                                      .path(SERVICE)
                                      .filters(f -> {
                                          // f.filter(requestFilter);
                                          return f;
                                      })
                                      .uri(URI))
                      .route("route2",
                              r -> r
                                      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                                      .and()
                                      .method(HttpMethod.POST)
                                      .and()
                                      .readBody(String.class, readBody -> {
                                          log.info("request method POST, Content-Type is application/x-www-form-urlencoded, body  is:{}", readBody);
                                          return true;
                                      })
                                      .and()
                                      .path(SERVICE)
                                      .filters(f -> {
                                          // f.filter(requestFilter);
                                          return f;
                                      })
                                      .uri(URI))
                      .route("route3",
                              r -> r
                                      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                                      .and()
                                      .method(HttpMethod.POST)
                                      .and()
                                      .readBody(Object.class, readBody -> {
                                          log.info("request method POST, Content-Type is application/json, body  is:{}", readBody);
                                          return true;
                                      })
                                      .and()
                                      .path(SERVICE)
                                      .filters(f -> {
                                          // f.filter(requestFilter);
                                          return f;
                                      })
                                      .uri(URI))
                      .build();
        //@formatter:on
    }

经过测试,该方案也可以获取参数,请求参数可以直接在过滤器中获取 。

其他 yml 路由的也可以使用这个方式获取参数。

Map<String, Object> data = exchange.getAttribute("cachedRequestBodyObject");

参考:

https://windmt.com/2019/01/16/spring-cloud-19-spring-cloud-gateway-read-and-modify-request-body/

https://blog.51cto.com/thinklili/2329184

修改接口返回报文

因为网关路由的接口返回报文格式各异,并且网关也有有一些限流、认证、熔断降级的返回报文,为了统一这些报文的返回格式,网关必须要对接口的返回报文进行修改,过滤器代码如下:

public class WrapperResponseFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        // -1 is response write filter, must be called before that
        return -2;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpResponse originalResponse = exchange.getResponse();
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        // probably should reuse buffers
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        // 释放掉内存
                        DataBufferUtils.release(dataBuffer);
                        String rs = new String(content, Charset.forName("UTF-8"));
                        Response response = new Response();
                        response.setCode("1");
                        response.setMessage("请求成功");
                        response.setData(rs);

                        byte[] newRs = JSON.toJSONString(response).getBytes(Charset.forName("UTF-8"));
                        originalResponse.getHeaders().setContentLength(newRs.length);//如果不重新设置长度则收不到消息。
                        return bufferFactory.wrap(newRs);
                    }));
                }
                // if body is not a flux. never got there.
                return super.writeWith(body);
            }
        };
        // replace response with decorator
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }
}
上次更新时间: 2024/5/7 05:59:02