Перейти к основному содержимому

Перезапись URL-адресов с помощью Spring Cloud Gateway

· 6 мин. чтения

1. Введение

Обычный вариант использования Spring Cloud Gateway — выступать в качестве фасада для одной или нескольких служб, предлагая клиентам более простой способ их использования.

В этом руководстве мы покажем различные способы настройки открытых API-интерфейсов путем перезаписи URL-адресов перед отправкой запроса на серверные части.

2. Краткий обзор Spring Cloud Gateway

Проект Spring Cloud Gateway построен на основе популярных Spring Boot 2 и Project Reactor , поэтому он наследует их основные преимущества:

  • Низкое потребление ресурсов благодаря реактивному характеру
  • Поддержка всех возможностей экосистемы Spring Cloud (обнаружение, настройка и т. д.)
  • Легко расширять и/или настраивать с помощью стандартных шаблонов Spring.

Мы уже рассмотрели его основные функции в предыдущих статьях, поэтому здесь мы просто перечислим основные концепции:

  • Маршрут : набор шагов обработки, через которые проходит соответствующий входящий запрос в шлюзе.
  • Predicate : Предикат Java 8 , который оценивается относительно ServerWebExchange .
  • Фильтры : экземпляры GatewayFilter , которые могут проверять и/или изменять ServerWebExchange . Шлюз поддерживает как глобальные фильтры, так и фильтры для каждого маршрута.

Вкратце, вот последовательность обработки входящего запроса:

  • Шлюз использует предикаты , связанные с каждым маршрутом, чтобы определить, какой из них будет обрабатывать запрос.
  • Как только маршрут найден, запрос ( экземпляр ServerWebExchange ) проходит через каждый настроенный фильтр, пока в конечном итоге не будет отправлен на серверную часть.
  • Когда серверная часть отправляет ответ обратно или возникает ошибка (например, тайм-аут или сброс соединения), фильтры снова получают возможность обработать ответ, прежде чем он будет отправлен обратно клиенту.

3. Перезапись URL-адреса на основе конфигурации

Возвращаясь к основной теме этой статьи, давайте посмотрим, как определить маршрут, который перезаписывает входящий URL-адрес перед его отправкой серверной части. Например, предположим, что при входящем запросе формы /api/v1/customer/* внутренний URL-адрес должен быть http://v1.customers/api/* . Здесь мы используем «*» для обозначения «всего, что находится за пределами этой точки».

Чтобы создать переписывание на основе конфигурации, нам просто нужно добавить несколько свойств в конфигурацию приложения . Здесь мы будем использовать конфигурацию на основе 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}

Разберем эту конфигурацию. Во-первых, у нас есть идентификатор маршрута, который является просто его идентификаторами. Далее у нас есть внутренний URI, заданный свойством uri . Обратите внимание, что учитываются только имя хоста/порт, поскольку окончательный путь исходит из логики перезаписи .

Свойство predicates определяет условия, которые должны быть выполнены для активации этого маршрута. В нашем случае мы используем предикат Path , который принимает выражение пути, подобное муравью, для сопоставления с путем входящего запроса.

Наконец, свойство filter имеет фактическую логику перезаписи. Фильтр RewritePath принимает два аргумента: регулярное выражение и строку замены. Реализация фильтра работает, просто выполняя метод replaceAll() для URI запроса, используя предоставленные параметры в качестве аргументов.

Предупреждение о том, как Spring обрабатывает файлы конфигурации, заключается в том, что мы не можем использовать стандартное выражение замены ${group} , так как Spring будет думать, что это ссылка на свойство, и попытается заменить его значение. Чтобы избежать этого, нам нужно добавить обратную косую черту между символами «$» и «{», которая будет удалена реализацией фильтра, прежде чем использовать ее в качестве фактического выражения замены.

4. Перезапись URL на основе DSL

Хотя RewritePath достаточно мощный и простой в использовании, он не работает в сценариях, где правило перезаписи имеет некоторые динамические аспекты. В зависимости от случая, все еще может быть возможно написать несколько правил, используя предикаты в качестве защиты для каждой ветви правила.

Однако, если это не так, мы можем создать маршрут, используя подход на основе DSL. Все, что нам нужно сделать, это создать bean-компонент RouteLocator , реализующий логику маршрута. В качестве примера создадим простой маршрут, который, как и прежде, переписывает входящий 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();
}
}

Здесь динамическая часть — это просто случайное число, добавленное к строке замены. Реальное приложение может иметь более сложную логику, но основной механизм будет таким же, как показано.

Несколько замечаний по шагам, через которые прошел этот код: во-первых, он вызывает addOriginalRequestUrl(), происходящий из класса ServerWebExchangeUtils , для сохранения исходного URL-адреса под атрибутом обмена GATEWAY_ORIGINAL_REQUEST_URL_ATTR . Значение этого атрибута представляет собой список, к которому мы добавим полученный URL-адрес перед выполнением каких-либо изменений и используем внутри шлюза как часть обработки заголовка X-Forwarded-For .

Во-вторых, как только мы применили логику перезаписи, мы должны сохранить измененный URL-адрес в атрибуте обмена GATEWAY_REQUEST_URL_ATTR . Этот шаг прямо не упоминается в документации, но гарантирует, что наш пользовательский фильтр хорошо работает с другими доступными фильтрами.

5. Тестирование

Чтобы проверить наши правила перезаписи, мы будем использовать стандартные классы JUnit 5 с небольшой поправкой: мы запустим простой сервер на основе класса com.sun.net.httpserver.HttpServer из Java SDK. Сервер запустится на случайном порту, что позволит избежать конфликтов портов.

Однако недостатком этого подхода является то, что нам нужно выяснить, какой порт был фактически назначен серверу, и передать его Spring, чтобы мы могли использовать его для установки свойства uri маршрута . К счастью, Spring предоставляет элегантное решение этой проблемы: @DynamicPropertySource . Здесь мы будем использовать его для запуска сервера и регистрации свойства со значением связанного порта:

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

Обработчик теста просто возвращает полученный 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. Заключение

В этом кратком руководстве мы показали различные способы перезаписи URL-адресов с помощью библиотеки Spring Cloud Gateway. Как обычно, весь код доступен на GitHub .