«1. Введение
Spring Cloud Gateway — это интеллектуальный прокси-сервис, часто используемый в микросервисах. Он прозрачно централизует запросы в единой точке входа и направляет их в соответствующий сервис. Одной из самых интересных его особенностей является концепция фильтров (WebFilter или GatewayFilter).
WebFilter вместе с фабриками Predicate включают полный механизм маршрутизации. Spring Cloud Gateway предоставляет множество встроенных фабрик WebFilter, которые позволяют взаимодействовать с HTTP-запросами до достижения прокси-сервиса и HTTP-ответами перед доставкой результата клиенту. Также возможно реализовать пользовательские фильтры.
В этом руководстве мы сосредоточимся на встроенных фабриках WebFilter, включенных в проект, и на том, как их использовать в расширенных случаях использования.
2. Фабрики WebFilter
Фабрики WebFilter (или GatewayFilter) позволяют изменять входящие HTTP-запросы и исходящие HTTP-ответы. В этом смысле он предлагает набор интересных функций, которые можно применять до и после взаимодействия с нижестоящими сервисами.
Отображение обработчиков управляет запросом клиента. Он проверяет, соответствует ли он какому-то сконфигурированному маршруту. Затем он отправляет запрос веб-обработчику для выполнения определенной цепочки фильтров для этого маршрута. Пунктирная линия разделяет логику на логику до и после фильтрации. Фильтры дохода запускаются перед запросом прокси. Выходные фильтры вступают в действие, когда они получают ответ прокси. Фильтры предоставляют механизмы для изменения промежуточного процесса.
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. Мы показали, как взаимодействовать с запросами и ответами от клиента до и после выполнения проксируемого сервиса.