«1. Введение

Spring Cloud Gateway — это интеллектуальный прокси-сервис, часто используемый в микросервисах. Он прозрачно централизует запросы в единой точке входа и направляет их в соответствующий сервис. Одной из самых интересных его особенностей является концепция фильтров (WebFilter или GatewayFilter).

WebFilter вместе с фабриками Predicate включают полный механизм маршрутизации. Spring Cloud Gateway предоставляет множество встроенных фабрик WebFilter, которые позволяют взаимодействовать с HTTP-запросами до достижения прокси-сервиса и HTTP-ответами перед доставкой результата клиенту. Также возможно реализовать пользовательские фильтры.

В этом руководстве мы сосредоточимся на встроенных фабриках WebFilter, включенных в проект, и на том, как их использовать в расширенных случаях использования.

2. Фабрики WebFilter

Фабрики WebFilter (или GatewayFilter) позволяют изменять входящие HTTP-запросы и исходящие HTTP-ответы. В этом смысле он предлагает набор интересных функций, которые можно применять до и после взаимодействия с нижестоящими сервисами.

Spring Cloud Gateway WebFilter Factories Architecture

Отображение обработчиков управляет запросом клиента. Он проверяет, соответствует ли он какому-то сконфигурированному маршруту. Затем он отправляет запрос веб-обработчику для выполнения определенной цепочки фильтров для этого маршрута. Пунктирная линия разделяет логику на логику до и после фильтрации. Фильтры дохода запускаются перед запросом прокси. Выходные фильтры вступают в действие, когда они получают ответ прокси. Фильтры предоставляют механизмы для изменения промежуточного процесса.

3. Реализация фабрик WebFilter

Давайте рассмотрим наиболее важные фабрики WebFilter, включенные в проект Spring Cloud Gateway. Есть два способа реализовать их, используя YAML или Java DSL. Мы покажем примеры того, как реализовать оба.

3.1. HTTP-запрос

Встроенные фабрики WebFilter позволяют взаимодействовать с заголовками и параметрами HTTP-запроса. Мы можем добавить (AddRequestHeader), сопоставить (MapRequestHeader), установить или заменить (SetRequestHeader) и удалить (RemoveRequestHeader) значения заголовков и отправить их в прокси-сервис. Исходный заголовок хоста также можно сохранить (PreserveHostHeader).

Таким же образом мы можем добавить (AddRequestParameter) и удалить (RemoveRequestParameter) параметры, которые будут обрабатываться нижестоящим сервисом. Давайте посмотрим, как это сделать:

- id: add_request_header_route
  uri: https://httpbin.org
  predicates:
  - Path=/get/**
  filters:
  - AddRequestHeader=My-Header-Good,Good
  - AddRequestHeader=My-Header-Remove,Remove
  - AddRequestParameter=var, good
  - AddRequestParameter=var2, remove
  - MapRequestHeader=My-Header-Good, My-Header-Bad
  - MapRequestHeader=My-Header-Set, My-Header-Bad
  - SetRequestHeader=My-Header-Set, Set 
  - RemoveRequestHeader=My-Header-Remove
  - RemoveRequestParameter=var2

Давайте проверим, все ли работает как положено. Для этого мы будем использовать curl и общедоступный httpbin.org:

$ curl http://localhost:8080/get
{
  "args": {
    "var": "good"
  },
  "headers": {
    "Host": "localhost",
    "My-Header-Bad": "Good",
    "My-Header-Good": "Good",
    "My-Header-Set": "Set",
  },
  "origin": "127.0.0.1, 90.171.125.86",
  "url": "https://localhost:8080/get?var=good"
}

Мы можем увидеть ответ curl как следствие настроенных фильтров запросов. Они добавляют My-Header-Good со значением Good и сопоставляют его содержимое с My-Header-Bad. Они удаляют My-Header-Remove и устанавливают новое значение My-Header-Set. В разделах args и url мы видим добавленный новый параметр var. Кроме того, последний фильтр удаляет параметр var2.

Кроме того, мы можем изменить тело запроса до того, как он достигнет прокси-сервиса. Этот фильтр можно настроить только с использованием нотации Java DSL. Фрагмент ниже просто заглавными буквами содержит содержимое тела ответа:

@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
     return builder.routes()
       .route("modify_request_body", r -> r.path("/post/**")
         .filters(f -> f.modifyRequestBody(
           String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, 
           (exchange, s) -> Mono.just(new Hello(s.toUpperCase()))))
         .uri("https://httpbin.org"))
       .build();
}

Чтобы проверить фрагмент, давайте выполним curl с параметром -d, чтобы включить тело «Content»:

$ curl -X POST "http://localhost:8080/post" -i -d "Content"
"data": "{\"message\":\"CONTENT\"}",
"json": {
    "message": "CONTENT"
}

Мы можем видеть, что содержимое тела теперь отображается в верхнем регистре как CONTENT в результате применения фильтра.

3.2. HTTP-ответ

Точно так же мы можем изменить заголовки ответа, используя добавление (AddResponseHeader), установку или замену (SetResponseHeader), удаление (RemoveResponseHeader) и перезапись (RewriteResponseHeader). Еще одна функция ответа — дедупликация (DedupeResponseHeader), чтобы перезаписать стратегии и избежать их дублирования. Мы можем избавиться от специфичных для бэкенда сведений о версии, расположении и хосте, используя другую встроенную фабрику (RemoveLocationResponseHeader).

Давайте рассмотрим полный пример:

- id: response_header_route
  uri: https://httpbin.org
  predicates:
  - Path=/header/post/**
  filters:
  - AddResponseHeader=My-Header-Good,Good
  - AddResponseHeader=My-Header-Set,Good
  - AddResponseHeader=My-Header-Rewrite, password=12345678
  - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
  - AddResponseHeader=My-Header-Remove,Remove
  - SetResponseHeader=My-Header-Set, Set
  - RemoveResponseHeader=My-Header-Remove
  - RewriteResponseHeader=My-Header-Rewrite, password=[^&]+, password=***
  - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,

Давайте используем curl для отображения заголовков ответа:

$ curl -X POST "http://localhost:8080/header/post" -s -o /dev/null -D -
HTTP/1.1 200 OK
My-Header-Good: Good
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
My-Header-Rewrite: password=***
My-Header-Set: Set

Аналогично HTTP-запросу, мы можем изменить тело ответа. В этом примере мы перезаписываем тело ответа PUT:

@Bean
public RouteLocator responseRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
      .route("modify_response_body", r -> r.path("/put/**")
        .filters(f -> f.modifyResponseBody(
          String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, 
          (exchange, s) -> Mono.just(new Hello("New Body"))))
        .uri("https://httpbin.org"))
      .build();
}

Давайте используем конечную точку PUT для проверки функциональности:

$ curl -X PUT "http://localhost:8080/put" -i -d "CONTENT"
{"message":"New Body"}

«

«3.3. Путь

- id: path_route
  uri: https://httpbin.org
  predicates:
  - Path=/new/post/**
  filters:
  - RewritePath=/new(?<segment>/?.*), $\{segment}
  - SetPath=/post

Одной из функций встроенных фабрик WebFilter является взаимодействие с путями, настроенными клиентом. Можно задать другой путь (SetPath), переписать (RewritePath), добавить префикс (PrefixPath) и удалить (StripPrefix), чтобы извлечь только его части. Помните, что фильтры выполняются в порядке их расположения в файле YAML. Давайте посмотрим, как настроить маршруты:

$ curl -X POST "http://localhost:8080/new/post" -i
"X-Forwarded-Prefix": "/new"
"url": "https://localhost:8080/post"

Оба фильтра удаляют подпуть /new до достижения прокси-сервиса. Давайте выполним curl:

Мы также могли бы использовать фабрику StripPrefix. С StripPrefix=1 мы можем избавиться от первого подпути при обращении к нижестоящей службе.

3.4. Относится к статусу HTTP

- id: redirect_route
  uri: https://httpbin.org
  predicates:
  - Path=/fake/post/**
  filters:
  - RedirectTo=302, https://httpbin.org
- id: status_route
  uri: https://httpbin.org
  predicates:
  - Path=/delete/**
  filters:
  - SetStatus=401

RedirectTo принимает два параметра: статус и URL. Статус должен представлять собой серию HTTP-кода перенаправления 300 и действительный URL-адрес. SetStatus принимает один статус параметра, который может быть кодом HTTP или его строковым представлением. Давайте рассмотрим пару примеров:

$ curl -X POST "http://localhost:8080/fake/post" -i
HTTP/1.1 302 Found
Location: https://httpbin.org

Первый фильтр действует по пути /fake/post, и клиент перенаправляется на https://httpbin.org с HTTP-статусом 302:

$ curl -X DELETE "http://localhost:8080/delete" -i
HTTP/1.1 401 Unauthorized

Второй фильтр определяет путь /delete и устанавливает HTTP-статус 401:

3.5. Предел размера запроса

- id: size_route
  uri: https://httpbin.org
  predicates:
  - Path=/anything
  filters:
  - name: RequestSize
    args:
       maxSize: 5000000

Наконец, мы можем ограничить предельный размер запроса (RequestSize). Если размер запроса превышает лимит, шлюз отклоняет доступ к сервису:

4. Расширенные варианты использования

Spring Cloud Gateway предлагает другие расширенные фабрики WebFilter для поддержки базовых функций шаблона микросервисов.

4.1. Circuit Breaker

- id: circuitbreaker_route
  uri: https://httpbin.org
  predicates:
  - Path=/status/504
  filters:
  - name: CircuitBreaker
  args:
     name: myCircuitBreaker
     fallbackUri: forward:/anything
  - RewritePath=/status/504, /anything

Spring Cloud Gateway имеет встроенную фабрику WebFilter для работы с Circuit Breaker. Фабрика допускает различные стратегии отката и конфигурацию маршрута Java DSL. Давайте рассмотрим простой пример:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

Для настройки автоматического выключателя мы использовали Resilience4J, добавив зависимость spring-cloud-starter-circuitbreaker-reactor-resilence4j:

$ curl http://localhost:8080/status/504 
"url": "https://localhost:8080/anything"

Опять же, мы можем протестировать функциональность с помощью curl:

4.2. Повтор

- id: retry_test
  uri: https://httpbin.org
  predicates:
  - Path=/status/502
  filters:
  - name: Retry
    args:
       retries: 3
       statuses: BAD_GATEWAY
       methods: GET,POST
       backoff:
          firstBackoff: 10ms
          maxBackoff: 50ms
          factor: 2
          basedOnPreviousValue: false

Еще одна расширенная функция позволяет клиенту повторить попытку доступа, когда что-то происходит с прокси-сервисами. Он принимает несколько параметров, таких как количество повторных попыток, коды состояния HTTP (состояния) и методы, которые следует повторить, серии, исключения и интервалы ожидания после каждой повторной попытки. Давайте посмотрим на конфигурацию YAML:

$ curl http://localhost:8080/status/502

Когда клиент достигает /status/502 (плохой шлюз), фильтр повторяет попытку три раза, ожидая интервалов отсрочки, настроенных после каждого выполнения. Давайте посмотрим, как это работает:

Mapping [Exchange: GET http://localhost:8080/status/502] to Route{id='retry_test', ...}
Handler is being applied: {uri=https://httpbin.org/status/502, method=GET}
Received last HTTP packet
Handler is being applied: {uri=https://httpbin.org/status/502, method=GET}
Received last HTTP packet
Handler is being applied: {uri=https://httpbin.org/status/502, method=GET}
Received last HTTP packet

В то же время нам нужно проверить журналы шлюза на сервере: получает статус 502.

4.3. Сохранить сеанс и безопасные заголовки

Фабрика SecureHeader добавляет в ответ заголовки безопасности HTTP. Точно так же SaveSession имеет особое значение при использовании с Spring Session и Spring Security:

filters: 
- SaveSession

Этот фильтр сохраняет состояние сеанса перед выполнением переадресованного вызова.

4.4. Ограничитель скорости запроса

И последнее, но не менее важное: фабрика RequestRateLimiter определяет, может ли запрос продолжаться. Если нет, он возвращает статус HTTP-кода 429 — слишком много запросов. Он использует различные параметры и преобразователи для указания ограничителя скорости.

RedisRateLimiter использует известную базу данных Redis для проверки количества токенов, которое может хранить корзина. Для этого требуется следующая зависимость:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
 </dependency>

Следовательно, ему также нужна конфигурация Spring Redis:

spring:
  redis:
    host: localhost
    port: 6379

Фильтр имеет несколько свойств. Первый аргумент, пополнениеRate, является допустимым количеством запросов в секунду. Второй аргумент, BurstCapacity, — это максимальное количество запросов за одну секунду. Третий параметр, requestTokens, указывает, сколько токенов стоит запрос. Давайте посмотрим на пример реализации:

- id: request_rate_limiter
  uri: https://httpbin.org
  predicates:
  - Path=/redis/get/**
  filters:
  - StripPrefix=1
  - name: RequestRateLimiter
    args:
       redis-rate-limiter.replenishRate: 10
       redis-rate-limiter.burstCapacity: 5

Давайте используем curl для тестирования фильтра. Заранее не забудьте запустить экземпляр Redis, например, с помощью Docker:

$ curl "http://localhost:8080/redis/get" -i
HTTP/1.1 200 OK
X-RateLimit-Remaining: 4
X-RateLimit-Requested-Tokens: 1
X-RateLimit-Burst-Capacity: 5
X-RateLimit-Replenish-Rate: 10

«

00:57:48.263 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->200, reason->OK, remaining->[4]
00:57:48.394 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->200, reason->OK, remaining->[3]
00:57:48.530 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->200, reason->OK, remaining->[2]
00:57:48.667 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->200, reason->OK, remaining->[1]
00:57:48.826 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->200, reason->OK, remaining->[0]
00:57:48.851 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->429, reason->Too Many Requests, remaining->[0]
00:57:48.894 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->429, reason->Too Many Requests, remaining->[0]
00:57:49.135 [main] INFO  c.b.s.w.RedisWebFilterFactoriesLiveTest - Received: status->200, reason->OK, remaining->[4]

«Как только оставшееся ограничение скорости достигает нуля, шлюз создает HTTP-код 429. Для тестирования поведения мы можем использовать модульные тесты. Мы запускаем встроенный сервер Redis и параллельно запускаем RepeatedTests. Когда ведро достигает предела, начинает отображаться ошибка:

5. Заключение

В этом руководстве мы рассмотрели фабрики веб-фильтров Spring Cloud Gateway. Мы показали, как взаимодействовать с запросами и ответами от клиента до и после выполнения проксируемого сервиса.