URL Rewriting With Spring Cloud Gateway – 用Spring Cloud Gateway重写URL

最后修改: 2022年 2月 23日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

A common use case for the Spring Cloud Gateway is to act as a facade to one or more services, thus offering clients a simpler way to consume them.

Spring云网关的一个常见用例是作为一个或多个服务的门面,从而为客户提供一种更简单的方式来消费它们。

In this tutorial, we’ll show different ways to customize the exposed APIs by rewriting the URLs before sending the request to the backends.

在本教程中,我们将展示在向后端发送请求之前通过重写URL来定制暴露的API的不同方法。

2. Spring Cloud Gateway Quick Recap

2.Spring的云网关快速回顾

The Spring Cloud Gateway project is built on top of the popular Spring Boot 2 and Project Reactor, so it inherits its main treats:

Spring Cloud Gateway项目是建立在流行的Spring Boot 2和Project Reactor之上的,因此它继承了其主要待遇。

  • Low resource usage, thanks to its reactive nature
  • Support for all goodies from the Spring Cloud ecosystem (discovery, configuration, etc.)
  • Easy to extend and/or customize using standard Spring patterns

We’ve already covered its main features in earlier articles, so here we’ll just list the  main concepts:

我们已经在以前的文章中介绍了它的主要功能,所以在这里我们只列出主要的概念。

  • Route: a set of processing steps that a matching incoming request goes through in the Gateway
  • Predicate: A Java 8 Predicate that gets evaluated against a ServerWebExchange.
  • Filters: GatewayFilter instances that can inspect and/or change a ServerWebExchange. The Gateway supports both global filters and per-route ones.

In a nutshell, here’s the processing sequence an incoming request goes through:

简而言之,这里是一个传入请求的处理顺序。

  • The Gateway uses the Predicates associated with each route to find which one will handle the request
  • Once a route is found, the request (a ServerWebExchange instance) goes through each configured filter until it is eventually sent to a backend.
  • When the backend sends a response back, or there’s an error (timeout or connection reset, for instance), filters get again a chance to process the response before it is sent back to the client.

3. Configuration-based URL Rewrite

3.基于配置的URL重写

Going back to this article’s main subject, let’s see how to define a route that rewrites the incoming URL before sending it to the backend. For example, suppose that given an incoming request of the form /api/v1/customer/*, the backend URL should be http://v1.customers/api/*. Here, we’re using “*” to represent “anything beyond this point”.

回到本文的主题,让我们看看如何定义一个路由,在将传入的URL发送到后端之前对其进行重写。例如,假设有一个形式为/api/v1/customer/*的传入请求,后端URL应该是http://v1.customers/api/*。在这里,我们用 “*”来表示 “超出这个点的任何东西”。

To create a configuration-based rewrite, we just need to add a few properties to the application’s configuration. Here, we’ll use YAML-based configuration for clarity, but this information could come from any supported PropertySource:

为了创建一个基于配置的重写,我们只需要在应用程序的配置中添加一些属性。在这里,为了清楚起见,我们将使用基于YAML的配置,但这些信息可以来自任何支持的PropertySource

spring:
  cloud:
    gateway:
      routes:
      - id: rewrite_v1
        uri: ${rewrite.backend.uri:http://example.com}
        predicates:
        - Path=/v1/customer/**
        filters:
        - RewritePath=/v1/customer/(?<segment>.*),/api/$\{segment}

Let’s dissect this configuration. Firstly, we have the route’s id, which is just its identifiers. Next, we have the backend URI given by the uri property. Notice that only hostname/port are considered, as the final path comes from the rewrite logic.

让我们来剖析一下这个配置。首先,我们有路由的id,这只是它的标识符。接下来,我们有由uri属性给出的后端URI。注意,我们只考虑主机名/端口,因为最终的路径来自重写逻辑

The predicates property defines the conditions that must be met to activate this route. In our case, we use the Path predicate, which takes an ant-like path expression to match against the path of the incoming request.

predicates属性定义了激活该路由必须满足的条件。在我们的例子中,我们使用了Path谓词,它需要一个类似蚂蚁的路径表达式来与传入请求的路径匹配。

Finally, the filters property has the actual rewrite logic. The RewritePath filter takes two arguments: a regular expression and a replacement string. The filter’s implementation works by simply executing the replaceAll() method on the request’s URI, using the provided parameters as arguments.

最后,filters属性有实际的重写逻辑。RewritePath过滤器需要两个参数:一个正则表达式和一个替换字符串。过滤器的实现是在请求的URI上简单地执行replaceAll()方法,使用提供的参数作为参数。

A caveat of the way that Spring handles configuration files is we can’t use the standard ${group} replacement expression, as Spring will think it is a property reference and try to replace its value. To avoid this, we need to add a backslash between the “$” and “{” characters that will be removed by the filter implementation before using it as the actual replacement expression.

Spring处理配置文件的方式的一个注意事项是,我们不能使用标准的${group}替换表达式,因为Spring会认为它是一个属性引用并试图替换其值。为了避免这种情况,我们需要在”$”和”{“字符之间添加一个反斜杠,在使用它作为实际的替换表达式之前,过滤器实现会将其删除。

4. DSL-based URL Rewrite

4.基于DSL的URL重写

While RewritePath is quite powerful and easy to use, it falls short in scenarios where the rewrite rule has some dynamic aspects. Depending on the case, it might still be possible to write multiple rules using predicates as guards for each branch of the rule.

虽然RewritePath相当强大且易于使用,但在重写规则有一些动态方面的情况下,它就显得不足了。根据不同的情况,可能仍然可以使用谓词作为规则的每个分支的守卫来编写多个规则。

However, if this is not the case, we can create a route using the DSL-based approach. All we need to do is create a RouteLocator bean that implements the route’s logic. As an example, let’s create a simple route that, as before, rewrites the incoming URI using a regular expression. This time, however, the replacement string will be dynamically generated on each request:

然而,如果不是这种情况,我们可以使用基于 DSL 的方法来创建路由。我们需要做的就是创建一个实现路由逻辑的 RouteLocator Bean。作为一个例子,让我们创建一个简单的路由,像以前一样,使用正则表达式重写传入的URI。但这次,替换字符串将在每个请求中动态生成。

@Configuration
public class DynamicRewriteRoute {
    
    @Value("${rewrite.backend.uri}")
    private String backendUri;
    private static Random rnd = new Random();
    
    @Bean
    public RouteLocator dynamicZipCodeRoute(RouteLocatorBuilder builder) {
        return builder.routes()
          .route("dynamicRewrite", r ->
             r.path("/v2/zip/**")
              .filters(f -> f.filter((exchange, chain) -> {
                  ServerHttpRequest req = exchange.getRequest();
                  addOriginalRequestUrl(exchange, req.getURI());
                  String path = req.getURI().getRawPath();
                  String newPath = path.replaceAll(
                    "/v2/zip/(?<zipcode>.*)", 
                    "/api/zip/${zipcode}-" + String.format("%03d", rnd.nextInt(1000)));
                  ServerHttpRequest request = req.mutate().path(newPath).build();
                  exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());
                  return chain.filter(exchange.mutate().request(request).build());
              }))
              .uri(backendUri))
          .build();
    }
}

Here, the dynamic part is just a random number appended to the replacement string. A real-world application might have more complex logic, but the essential mechanism would be the same as shown.

这里,动态部分只是将一个随机数附加到替换字符串上。现实世界的应用可能有更复杂的逻辑,但基本机制与图示相同。

A few remarks about the steps this code went through: Firstly, it calls the addOriginalRequestUrl(), which comes from the ServerWebExchangeUtils class, to store the original URL under the exchange’s attribute GATEWAY_ORIGINAL_REQUEST_URL_ATTR. The value of this attribute is a list to which we’ll append the received URL before going any modification and used internally by the gateway as part of the X-Forwarded-For header’s handling.

关于这段代码所经历的步骤,我想说几句。首先,它调用addOriginalRequestUrl(),,它来自ServerWebExchangeUtils类,将原始URL存储在交易所的属性GATEWAY_ORIGINAL_REQUEST_URL_ATTR下。这个属性的值是一个列表,在进行任何修改之前,我们将把收到的URL追加到这个列表中,并由网关内部使用,作为X-Forwarded-For头处理的一部分。

Secondly, once we’ve applied the rewrite logic, we must save the modified URL in the GATEWAY_REQUEST_URL_ATTR exchange’s attribute. This step is not directly mentioned in the documentation but ensures that our custom filter plays nicely with other available filters.

其次,一旦我们应用了重写逻辑,我们必须在GATEWAY_REQUEST_URL_ATTR交换的属性中保存修改的URL。这一步在文档中没有直接提到,但可以确保我们的自定义过滤器与其他可用的过滤器很好地配合。

5. Testing

5.测试

To test our rewrite rules, we’ll use standard JUnit 5 classes with a small twist: we’ll spin up a simple server based on Java SDK’s com.sun.net.httpserver.HttpServer class. The server will start on a random port, thus avoiding port conflicts.

为了测试我们的重写规则,我们将使用标准的JUnit 5类,但有一个小插曲:我们将基于Java SDK的com.sun.net.httpserver.HttpServer类旋转起一个简单的服务器。该服务器将在一个随机的端口上启动,从而避免了端口冲突。

The downside of this approach, however, is we have to find out which port was actually assigned to the server and pass it to Spring, so we can use it to set the route’s uri property. Fortunately, Spring provides us with an elegant solution for this problem: @DynamicPropertySource. Here, we’ll use it to start the server and register a property with the bound port’s value:

然而,这种方法的缺点是我们必须找出实际分配给服务器的端口,并将其传递给 Spring,以便我们可以使用它来设置路由的uri属性。幸运的是,Spring为我们提供了解决这一问题的优雅方案:@DynamicPropertySource.在这里,我们将使用它来启动服务器并注册一个具有绑定端口值的属性。

@DynamicPropertySource
static void registerBackendServer(DynamicPropertyRegistry registry) {
    registry.add("rewrite.backend.uri", () -> {
        HttpServer s = startTestServer();
        return "http://localhost:" + s.getAddress().getPort();
    });
}

The test handler simply echoes back the received URI in the response body. This allows us to verify that the rewrite rules work as expected. For instance, this is the

测试处理程序只是在响应体中回显收到的URI。这使我们能够验证重写规则是否按预期工作。例如,这就是

@Test
void testWhenApiCall_thenRewriteSuccess(@Autowired WebTestClient webClient) {
    webClient.get()
      .uri("http://localhost:" + localPort + "/v1/customer/customer1")
      .exchange()
      .expectBody()
      .consumeWith((result) -> {
          String body = new String(result.getResponseBody());
          assertEquals("/api/customer1", body);
      });
}

6. Conclusion

6.结语

In this quick tutorial, we’ve shown different ways to rewrite URLs using the Spring Cloud Gateway library. As usual, all code is available over on GitHub.

在这个快速教程中,我们展示了使用Spring Cloud Gateway库重写URL的不同方法。像往常一样,所有的代码都可以在GitHub上找到