1. Введение
Spring Cloud Gateway — это интеллектуальный прокси-сервис, часто используемый в микросервисах. Он прозрачно централизует запросы в единой точке входа и направляет их в соответствующий сервис. Одной из самых интересных его особенностей является концепция фильтров ( WebFilter
или GatewayFilter
).
WebFilter
вместе с Predicate factory
включает в себя полный механизм маршрутизации. Spring Cloud Gateway предоставляет множество встроенных фабрик WebFilter
, которые позволяют взаимодействовать с HTTP-запросами до достижения прокси-сервиса и HTTP-ответами перед доставкой результата клиенту . Также возможно реализовать пользовательские фильтры .
В этом руководстве мы сосредоточимся на встроенных фабриках WebFilter
, включенных в проект, и на том, как их использовать в расширенных случаях использования.
2. Фабрики веб- фильтров
Фабрики 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"
}
Мы можем видеть, что содержимое тела теперь в верхнем регистре до СОДЕРЖИМОГО
в результате фильтра.
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. Дорожка
Одной из возможностей встроенных фабрик WebFilter
является взаимодействие с настроенными клиентом путями . Можно задать другой путь ( SetPath
), переписать ( RewritePath
), добавить префикс ( PrefixPath
) и удалить ( StripPrefix
), чтобы извлечь только его части. Помните, что фильтры выполняются в порядке их расположения в файле YAML. Давайте посмотрим, как настроить маршруты:
- id: path_route
uri: https://httpbin.org
predicates:
- Path=/new/post/**
filters:
- RewritePath=/new(?<segment>/?.*), $\{segment}
- SetPath=/post
Оба фильтра удаляют подпуть /new
до достижения прокси-сервиса. Выполним завиток:
$ curl -X POST "http://localhost:8080/new/post" -i
"X-Forwarded-Prefix": "/new"
"url": "https://localhost:8080/post"
Мы также могли бы использовать фабрику StripPrefix
. С StripPrefix=1
мы можем избавиться от первого подпути при обращении к нижестоящей службе.
3.4. Связано со статусом HTTP
RedirectTo
принимает два параметра: статус и URL. Статус должен представлять собой серию HTTP-кода перенаправления 300 и действительный URL-адрес. SetStatus
принимает один статус параметра, который может быть кодом 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
Первый фильтр действует по пути /fake/post
, и клиент перенаправляется на https://httpbin.org
с HTTP-статусом 302 :
$ curl -X POST "http://localhost:8080/fake/post" -i
HTTP/1.1 302 Found
Location: https://httpbin.org
Второй фильтр определяет путь /delete
и устанавливает HTTP-статус 401:
$ curl -X DELETE "http://localhost:8080/delete" -i
HTTP/1.1 401 Unauthorized
3.5. Ограничение размера запроса
Наконец, мы можем ограничить размер запроса ( RequestSize
). Если размер запроса превышает лимит, шлюз отказывает в доступе к сервису :
- id: size_route
uri: https://httpbin.org
predicates:
- Path=/anything
filters:
- name: RequestSize
args:
maxSize: 5000000
4. Расширенные варианты использования
Spring Cloud Gateway предлагает другие расширенные фабрики WebFilter
для поддержки базовых функций шаблона микросервисов.
4.1. Автоматический выключатель
Spring Cloud Gateway имеет встроенную фабрику WebFilter
для работы с прерывателями цепи . Фабрика допускает различные стратегии отката и конфигурацию маршрута Java DSL. Давайте посмотрим на простой пример:
- id: circuitbreaker_route
uri: https://httpbin.org
predicates:
- Path=/status/504
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/anything
- RewritePath=/status/504, /anything
Для настройки автоматического выключателя мы использовали Resilience4J , добавив зависимость spring-cloud-starter-circuitbreaker-reactor-resilience4j
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
Опять же, мы можем проверить функциональность с помощью curl:
$ curl http://localhost:8080/status/504
"url": "https://localhost:8080/anything"
4.2. Повторить попытку
Еще одна расширенная функция позволяет клиенту повторить попытку доступа, когда что-то происходит с проксируемыми службами . Он принимает несколько параметров, таких как количество повторных попыток
, коды состояния HTTP ( статусы
) и методы
, которые следует повторять, серии
, исключения
и интервалы
ожидания после каждой повторной попытки. Давайте посмотрим на конфигурацию YAML:
- 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
Когда клиент достигает /status/502
(плохой шлюз), фильтр повторяет попытку три раза, ожидая интервалов отсрочки, настроенных после каждого выполнения. Давайте посмотрим, как это работает:
$ curl http://localhost:8080/status/502
Заодно нам нужно проверить логи Gateway на сервере:
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
Фильтр повторяет три попытки с этой задержкой для методов GET и POST, когда шлюз получает статус 502.
4.3. Сохранить сеанс и защитить заголовки
Фабрика SecureHeader
добавляет в ответ заголовки безопасности HTTP . Точно так же SaveSession
имеет особое значение при использовании с Spring Session
и Spring Security
:
filters:
- SaveSession
Этот фильтр сохраняет состояние сеанса перед выполнением переадресованного вызова .
4.4. Ограничитель скорости запроса
И последнее, но не менее важное: фабрика RequestRateLimiter
определяет, может ли запрос продолжаться . Если нет, он возвращает статус HTTP-кода 429 — Too Many Requests
. Он использует различные параметры и преобразователи для указания ограничителя скорости .
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
Как только оставшееся ограничение скорости достигает нуля, шлюз создает HTTP-код 429. Для тестирования поведения мы можем использовать модульные тесты. Мы запускаем встроенный сервер Redis и параллельно запускаем RepeatedTests .
Как только ведро достигает предела, начинает отображаться ошибка:
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]
5. Вывод
В этом руководстве мы рассмотрели фабрики WebFilter Spring Cloud Gateway.
Мы показали, как взаимодействовать с запросами и ответами от клиента до и после выполнения проксируемого сервиса.
Как всегда, код доступен на GitHub .