https://github.com/romanow/gateway-lecture
Report and example Spring Cloud Gateway for API Gateway
https://github.com/romanow/gateway-lecture
api-gateway spring-boot spring-cloud spring-cloud-gateway
Last synced: about 1 year ago
JSON representation
Report and example Spring Cloud Gateway for API Gateway
- Host: GitHub
- URL: https://github.com/romanow/gateway-lecture
- Owner: Romanow
- Created: 2022-08-30T19:08:59.000Z (almost 4 years ago)
- Default Branch: master
- Last Pushed: 2024-09-11T10:28:58.000Z (almost 2 years ago)
- Last Synced: 2025-04-16T02:09:28.522Z (about 1 year ago)
- Topics: api-gateway, spring-boot, spring-cloud, spring-cloud-gateway
- Language: Kotlin
- Homepage:
- Size: 75.3 MB
- Stars: 4
- Watchers: 1
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Как нам Spring Cloud Gateway жить помогает

[](https://github.com/pre-commit/pre-commit)
## Аннотация
Наверняка вы сталкивались с ситуацией, когда на сервисах нужно реализовать какую-то однотипную техническую логику (
например, логирование пользовательских запросов, security и т.п.), каким-то образом модифицировать запрос и пробросить
его дальше или просто сделать routing из одной точки, а не забивать url всех сервисов на frontend'е?
Поговорим про паттерн API Gateway, и как пример реализации возьмем Spring Cloud Gateway. В режиме Live Coding с его
помощью реализуем типовые операции: routing, модификация запросов и ответов, а так же прикрутим к нему security.
## План
1. Разберемся что такое паттерн API Gateway:
* какие проблемы он решает;
* а для чего его использовать не надо.
2. В двух словах про Spring Cloud Gateway и WebFlux.
3. Посмотрим что Spring Cloud Gateway умеет и чем он нам будет полезен:
* Настраиваем проксирование запросов.
* Добавляем заголовки.
* Реализуем Rate Limiter для запросов пользователя.
* Логируем запрос и ответ.
4. Добавляем retry и таймауты на запросы.
5. Подключаем Spring Cloud Security для защиты наших endpoints.
## Доклад
### Разберемся что такое паттерн API Gateway
Шлюз API находится между клиентами и службами, он выполняет функцию обратного прокси, передавая запросы от клиентов к
сервисам. Также он может выполнять такие специализированные задачи, как аутентификация, SSL-termination и Rate Limiting.
Функции Gateway API можно сгруппировать в соответствии со следующими задачами:
* routing – используется в качестве обратного прокси-сервера для перенаправления запросов на одну или несколько сервисов
с помощью маршрутизации L7. Шлюз предоставляет одну конечную точку для клиентов и позволяет разделить клиенты и
сервисы;
* повторное выполнение запросов (retry), Circuit Breaker, timeouts;
* безопасность – аутентификация и авторизация, black/white list и т.п. (это очень важный пункт, т.к. на API Gateway
решается вопрос безопасности, а за ним находится доверенна зона (DMZ));
* логирование запросов и ответов, мониторинг;
* кэширование ответов, gzip;
* агрегация и модификация запросов / ответов.
API Gateway в первую очередь утильный элемент системы, который _не должен_ содержать в себе бизнес логики. Но это идет в
противоречие с последним пунктом: "агрегация и модификация запросов / ответов", – потому что для любой манипуляции
данными требуется знать из каких блоков они состоят и как их собирать. А значит на Gateway переносится часть бизнес
логики, что делает его связанным с самим сервисом. Этого стоит избегать, но если есть необходимость в таком функционале,
то использовать его только для "обогащения" ответа, а не изменения его структуры.
### В двух словах про Spring Cloud Gateway и WebFlux
### Посмотрим что Spring Cloud Gateway умеет
#### Route Predicate как способ настроить гибкие правила проксирования
На базе этих правил Spring Cloud Gateway выполняет routing.
[Writing Custom Route Predicate Factories](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#writing-custom-route-predicate-factories)
Основные правила:
###### Path (Ant-Style формат)
```yaml
spring:
cloud:
gateway:
routes:
- id: path-route
uri: http://dictionary:8080
predicates:
- Path=/dict/**
```
[The Path Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-path-route-predicate-factory)
###### Header
```yaml
spring:
cloud:
gateway:
routes:
- id: header-route
uri: http://dictionary:8080
predicates:
- Header=X-Target-Service, dict
```
[The Header Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-header-route-predicate-factory)
###### Method
```yaml
spring:
cloud:
gateway:
routes:
- id: method-route
uri: http://dictionary:8080
predicates:
- Method=GET,POST
```
[The Method Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-method-route-predicate-factory)
###### Query Params
```yaml
spring:
cloud:
gateway:
routes:
- id: query-route
uri: http://dictionary:8080
predicates:
- Query=service, dict
```
[The Query Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-query-route-predicate-factory)
#### CORS
_Cross-Origin Resource Sharing (CORS)_ — механизм, использующий дополнительные HTTP-заголовки, чтобы дать возможность
агенту пользователя получать разрешения на доступ к выбранным ресурсам с сервера на источнике (домене), отличном от
того, что сайт использует в данный момент.
Источник идентифицируется следующей тройкой параметров: схема, полностью определенное имя хоста и порт.
Например, `http://example.com` и `https://example.com` имеют разные источники: первый использует схему `http`, а второй
`https`. Следовательно, 2 источника отличаются схемой и портом, тогда как хост один и тот же (`example.com`).
Таким образом, если хотя бы один из трех элементов у двух ресурсов отличается, то источник ресурсов также считается
разным. В кросс-доменный запрос браузер автоматически добавляет заголовок `Origin`, содержащий домен, с которого
осуществлён запрос. Сервер должен, со своей стороны, ответить специальными заголовками, разрешает ли он такой запрос к
себе. Если сервер разрешает кросс-доменный запрос с этого домена – он должен добавить к ответу заголовок
`Access-Control-Allow-Origin`, содержащий домен запроса или звёздочку `*`.
```yaml
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- PATCH
- PUT
- DELETE
```
```shell
$ curl -X OPTIONS \
-H 'Access-Control-Request-Method: GET' \
-H "Origin: http://localhost" \
http://localhost:8000/dict/v1/lego-sets/ -v
* Connected to localhost (127.0.0.1) port 8000 (#0)
* Server auth using Basic with user 'ronin'
> OPTIONS /dict/v1/lego-sets/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.79.1
> Accept: */*
> Access-Control-Request-Method: GET
> Origin: http://localhost
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET,POST,PATCH,PUT,DELETE
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< Referrer-Policy: no-referrer
< content-length: 0
<
* Connection #0 to host localhost left intact
```
#### Gateway Filter Factory как средство модификации запросов
[Writing Custom GatewayFilter Factories](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#writing-custom-gatewayfilter-factories)
##### Модификация path (`StripPrefixGatewayFactory`, `PrefixPathGatewayFilterFactory`)
Target URL: `http://localhost:8080/api/v1/lego-sets`, Gateway URL: `http://localhost:8080/dict/v1/lego-sets`.
```java
public class WebConfiguration {
@Bean
public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {
return builder
.routes()
.route("dictionary", pathSpec -> pathSpec
.path("/dict/**") // http://localhost:8080/dict/**
.filters(filterSpec -> filterSpec
.stripPrefix(1) // /dict/v1/lego-sets -> /v1/lego-sets
.prefixPath("/api")) // /v1/lego-sets -> /api/v1/lego-sets
.uri("http://localhost:8080"))
.build();
}
}
```
##### Добавляем заголовок (`AddRequestHeaderGatewayFilterFactory`)
```java
public class WebConfiguration {
@Bean
public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {
return builder
.routes()
.route("dictionary", pathSpec -> pathSpec
.path("/dict/**")
.filters(filterSpec -> filterSpec
.addRequestHeader("X-Gateway-Timestamp", ISO_DATE_TIME.format(now()))
.uri(routes.getDictionary()))
.uri("http://localhost:8080"))
.build();
}
}
```
##### Контроль пропускной способности сервиса (`RequestRateLimiterGatewayFilterFactory`)
Rate Limiter – ограничение скорости обработки запросов, т.е. это искусственный барьер, который не дает клиенту выполнить
больше определенного количества больше операций в единицу времени. Rate Limiter защищает систему от перегрузки, т.е. на
целевой сервис попадет только такое количество запросов, которое не приведет к дефициту ресурсов (Resource Starvation).
Так же Rate Limiter может использоваться для предотвращения brute force атак и для контроля доступа к платным ресурсам.
Spring Cloud Gateway предоставляет фильтр `RequestRateLimiterGatewayFilterFactory` для реализации Rate Limiter, но
оставляет за пользователем выбор ключа (`KeyResolver`) и самого алгоритма (`AbstractRateLimiter`).
Для простоты реализации считать, что Gateway запускаем в один instance или нам не требуется распространять информацию о
состоянии buckets между нодами.
Для реализации возьмем алгоритм Token Bucket (Алгоритм маркерной корзины):

Bucket – емкость конечного размера, ассоциированная с пользователем (ip, location и т.п.), куда помещаются маркеры. Если
количество маркеров больше заданного объема, то запрос отбрасывается (429 Too Many Requests), иначе в bucket добавляется
token и запрос продолжает выполнение. Количество token в корзине возобновляется в течение времени.
[InMemoryRateLimiter](gateway/src/main/java/ru/romanow/gateway/utils/InMemoryRateLimiter.kt)
```java
@Configuration
public class WebConfiguration {
@Bean
public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {
return builder
.routes()
.route("dictionary", pathSpec -> pathSpec
.path("/dict/**")
.filters(filterSpec -> filterSpec
.requestRateLimiter(rateLimiterConfig -> rateLimiterConfig
.setRateLimiter(rateLimiter())
.setKeyResolver(keyResolver())))
.uri(routes.getDictionary()))
.build();
}
@Bean
public RateLimiter rateLimiter() {
return new InMemoryRateLimiter(1, 2, ofSeconds(10));
}
@Bean
public KeyResolver keyResolver() {
return exchange -> just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
```
[System Design Basics: Rate Limiter](https://medium.com/geekculture/system-design-basics-rate-limiter-351c09a57d14)
##### Повтор запросов (`RetryGatewayFilterFactory`)
Если запрос завершился неуспешно (5xx Series), то выполняем повторный запрос. Spring Cloud Gateway позволяет настраивать
политику повтора:
* retries – количество повторов;
* backoff – экспоненциальная задержка перед следующим повтором.
```java
@Configuration
public class WebConfiguration {
@Bean
public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {
// @formatter:off
return builder
.routes()
.route("dictionary", pathSpec -> pathSpec
.path("/dict/**")
.filters(filterSpec -> filterSpec
.retry(retryConfig -> retryConfig
.setRetries(3) // 3 повтора
.setStatuses(HttpStatus.NOT_FOUND) // включить повтор при 404 Not Found
.setSeries(HttpStatus.Series.SERVER_ERROR) // выполнять повтор при 5xx ошибках
.setBackoff(ofSeconds(1), // firstBackoff – первый повтор через 1 секунду,
ofSeconds(10), // maxBackoff – не более 10 секунд
2, // factor – коэффициент задержки: backoff = firstBackoff * (factor ^ n), т.е.
false))) // basedOnPreviousValue – если true, то backoff = prevBackoff * factor
// если basedOnPreviousValue = true, то prevBackoff * factor
.uri(routes.getDictionary()))
.build();
// @formatter:on
}
}
```
##### Модификация тела ответа (`ModifyResponseBodyGatewayFilterFactory`)
#### Таймауты запросов
Если мы знаем, что 99% запросов (99 line) выполняется за 200ms, не имеет смысла ждать окончания выполнения операции
больше 400ms. Это называется Fail Fast – если операция выполняется слишком долго, то ее лучше прервать и повторить еще
раз.
Для задания таймаутов в Java HTTP клиентах можно задать:
* `Connection Timeout` (установка соединения).
* `Read Timeout` (таймаут чтения из InputStream после установки соединения).
Источник: [Class URLConnection :: setReadTimeout](https://docs.oracle.com/javase/8/docs/api/java/net/URLConnection.html#setReadTimeout-int-).
```java
@Configuration
public class WebConfiguration {
@Bean
@Autowired
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(500))
.setReadTimeout(Duration.ofSeconds(500))
.build();
}
}
```
Если мы используем API Gateway, то мы можем задавать суммарный таймаут на всю операцию. В Spring Cloud Gateway можно
задать общий таймаут и отдельный таймаут на каждую операцию:
```java
@Configuration
public class WebConfiguration {
@Bean
public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {
return builder
.routes()
.route("dictionary", spec -> spec
.path("/dict/**")
.metadata(RouteMetadataUtils.RESPONSE_TIMEOUT_ATTR, 2000)
.uri(routes.getDictionary()))
.build();
}
}
```
### Подключаем Spring Cloud Security для защиты наших endpoints
Одна из основных задач API Gateway – авторизация, реализуем ее с помощью `Spring Security`. Т.к. Spring Cloud Gateway
построен на WebFlux, для настройки правил security требуется создать `SecurityWebFilterChain` (отличии от WebMVC, где мы
наследовались от `WebSecurityConfigurerAdapter` и задавали конфигурацию `HttpSecurity`).
```kotlin
@Configuration
class SecurityConfiguration {
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain =
http.authorizeExchange {
it.pathMatchers(OPTIONS).permitAll() // для CORS Preflight Request
it.pathMatchers(GET, "/manage/**").permitAll() // Actuator Endpoints
it.anyExchange().authenticated() // Все остальное с Basic Auth
}
.httpBasic { }
.build()
}
```
### Дополнительные возможности
Можно посмотреть настройки проксирования и примененные правила:
```shell
$ curl http://localhost:8000/manage/gateway/routes | jq
```
По конкретному route:
```shell
$ curl http://localhost:8000/manage/gateway/routes/dictionary | jq
```
## Запуск примера
```shell
# запускает postgres и dictionary
$ docker compose up -d --wait
$ ./gradlew gateway:bootRun --args='--spring.profiles.active=local'
```
## Ссылки
1. [Spring Cloud Gateway](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html)
2. [Bucket4j Rate Limiter](https://github.com/bucket4j/bucket4j)
## TODO
1. Описать проблематику: какие сложности у нас будут без Gateway.