Share
Embed
Как нам Spring Cloud Gateway жить помогает
Наверняка вы сталкивались с ситуацией, когда на сервисах нужно реализовать какую-то однотипную техническую логику ( например, логирование пользовательских запросов, security и т.п.), каким-то образом модифицировать запрос и пробросить его дальше или просто сделать routing из одной точки, а не забивать url всех сервисов на frontend'е?
Поговорим про паттерн API Gateway, и как пример реализации возьмем Spring Cloud Gateway. В режиме Live Coding с его помощью реализуем типовые операции: routing, модификация запросов и ответов, а так же прикрутим к нему security.
Romanov Alexey
November 02, 2026
Transcript
31 slides
-
СОДЕРЖАНИЕ + Разберемся что такое паттерн API Gateway: • какие
проблемы он решает; • а для чего его использовать не надо. + В двух словах про Spring Cloud Gateway и WebFlux. + Посмотрим что Spring Cloud Gateway умеет и чем он нам будет полезен: • Настраиваем проксирование запросов. • Добавляем заголовки. • Реализуем Rate Limiter для запросов пользователя. • Логируем запрос и ответ. • Делаем retry неуспешных запросов. + Подключаем Spring Cloud Security для защиты наших endpoints. -
О СПИКЕРЕ • В индустрии более 13 лет • Основная
специализация – Java / Kotlin • Работаю в Иннотехе в роли Software Архитектора уже 3.5 года • Мы с вами встречались на других конференциях 🙂 3 -
ЧТО ТАКОЕ API GATEWAY Gateway API находится между клиентами и
службами, он выполняет функцию обратного proxy, передавая информацию от клиентов к сервисам. Основные задачи, которые он решает: + Routing – используется в качестве обратного прокси-сервера для перенаправления запросов на одну или несколько сервисов с помощью маршрутизации L7. + Повторное выполнение запросов (retry), Circuit Breaker, timeouts. + Безопасность: аутентификация и авторизация, black/white list и т.п. (это очень важный пункт, т.к. на API Gateway решается вопрос безопасности, а за ним находится доверенна зона (DMZ)). + Логирование запросов и ответов, мониторинг. + Агрегация и модификация запросов / ответов. 4 -
ДЛЯ ЧЕГО НЕ НУЖНО ИСПОЛЬЗОВАТЬ API GATEWAY API Gateway –
утильный элемент системы, который не должен содержать в себе бизнес логики. 5 -
ДЛЯ ЧЕГО НЕ НУЖНО ИСПОЛЬЗОВАТЬ API GATEWAY На предыдущем слайде:
Агрегация и модификация запросов / ответов. Для любой манипуляции данными требуется знать из каких блоков они состоят и как их собирать. А значит на API Gateway переносится часть бизнес логики, что делает его связанным с самим сервисом. Этого стоит избегать, но если есть необходимость в таком функционале, то использовать его только для обогащения ответа, а не изменения его структуры. 6 -
ПРИМЕР ИСПОЛЬЗОВАНИЯ i /d /dict/v1/lego-sets c * t/ * Dictionary
Service + GET /api/v1/lego-sets API Gateway + GET /api/v1/lego-sets/{number} + GET /api/v1/series + GET /api/v1/series/{name} 7 -
ПРОКСИРОВАНИЕ ЗАПРОСОВ 1. @Configuration 2. class WebConfiguration { 3. 4.
@Bean 5. fun routers(builder: RouteLocatorBuilder) = 6. builder.routes { 7. route(id = "dictionary") { 8. path("/dict/**") 9. filters { 10. stripPrefix(1) 11. prefixPath("/api") 12. } 13. uri("http://localhost:8080") 14. } 15. } 16.} 9 -
ДОБАВЛЯЕМ ЗАГОЛОВКИ К ОТВЕТУ 1. @Configuration 2. class WebConfiguration {
3. 4. @Bean 5. fun routers(builder: RouteLocatorBuilder) = 6. builder.routes { 7. route(id = "dictionary") { 8. path("/dict/**") 9. filters { 10. stripPrefix(1) 11. prefixPath("/api") 12. addResponseHeader("X-Gateway-Timestamp", now()) 13. } 14. uri("http://localhost:80800") 15. } 16. } 17.} 11 -
ПАТТЕРН RATE LIMITER Rate Limiter – ограничение скорости обработки запросов,
т.е. это искусственный барьер, который не дает клиенту выполнить больше определенного количества больше операций в единицу времени. Rate Limiter защищает систему от перегрузки, т.е. на целевой сервис попадет только такое количество запросов, которое не приведет к дефициту ресурсов (Resource Starvation). Так же Rate Limiter может использоваться для предотвращения Brute Force атак и для контроля доступа к платным ресурсам. 13 -
TOKEN BUCKET ALGORITHM Bucket – емкость конечного размера, ассоциированная с
пользователем (ip, location и т.п.), куда помещаются маркеры. Если количество маркеров больше заданного объема, то запрос отбрасывается (429 Too Many Requests), иначе в bucket добавляется token и запрос продолжает выполнение. Количество token в корзине возобновляется в течение времени. 14 -
ПАТТЕРН RATE LIMITER $ curl http://localhost:8000/dict/v1/lego-sets/ -v > GET /dict/v1/lego-sets/
HTTP/1.1 > Host: localhost:8000 > < HTTP/1.1 200 OK < X-RateLimit-Remaining: 0 < X-RateLimit-Replenish-Rate: 1 < X-RateLimit-Burst-Capacity: 2 < Content-Type: application/json < Cache-Control: no-cache, no-store, max-age=0, must-revalidate 15 -
ПАТТЕРН RATE LIMITER 1. @Configuration 2. class WebConfiguration { 3.
@Bean 4. fun routers(builder: RouteLocatorBuilder, properties: ApplicationProperties) = 5. builder.routes { 6. route(id = "dictionary") { 7. path("/dict/**") 8. filters { 9. ... 10. requestRateLimiter { 11. it.rateLimiter = rateLimiter(properties) 12. it.keyResolver = keyResolver() 13. } 14. }}} 15. 16. fun rateLimiter(properties: ApplicationProperties) = 17. InMemoryRateLimiter(replenishRate = 1, burstCapacity = 2, duration = ofSeconds(1)) 18. 19. fun keyResolver() = KeyResolver { Mono.just(it.request.remoteAddress.address.hostAddress) } 20. } 16 -
ЛОГГИРУЕМ ЗАПРОС И ОТВЕТ 1. @Configuration 2. class WebConfiguration {
3. 4. @Bean 5. fun routers(builder: RouteLocatorBuilder, properties) = 6. builder.routes { 7. route(id = "dictionary") { 8. path("/dict/**") 9. filters { 10. ... 11. modifyRequestBody(String::class.java, String::class.java, 12. LoggingHandler("Request")) 13. modifyResponseBody(String::class.java, String::class.java, 14. LoggingHandler("Response")) 15. } 16. uri("http://localhost:8080") 17. } 18. } 19. } 18 -
ЛОГГИРУЕМ ЗАПРОС И ОТВЕТ 1. class LoggingHandler(val operation: String): RewriteFunction<String,
String> { 2. private val logger = LoggerFactory.getLogger(LoggingHandler::class.java) 3. 4. override fun apply(e: ServerWebExchange, body: String): Publisher<*> { 5. logger.info( 6. "{} {} {}: {}", operation, request.method, request.path, 7. if (body != null) ellipsis(body, 255) else "[empty body]" 8. ) 9. return if (body != null) Mono.just(body) else Mono.empty() 10. } 11.} 19 -
ЛОГГИРУЕМ ЗАПРОС И ОТВЕТ 1. return new GatewayFilter() { 2.
@Override 3. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 4. ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); 5. Mono<?> modifiedBody = serverRequest.bodyToMono(inClass) 6. .flatMap(body -> config.rewriteFunction().apply(exchange, body)) 7. .switchIfEmpty(defer(() -> config.rewriteFunction().apply(exchange, null))); 8. 9. BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); 10. 11. CachedBodyOutputMessage outputMessage = 12. new CachedBodyOutputMessage(exchange, headers); 13. return bodyInserter.insert(outputMessage, new BodyInserterContext()) 14. .then(Mono.defer(() -> { 15. ServerHttpRequest decorator = decorate(exchange, headers, outputMessage); 16. return chain.filter(exchange.mutate().request(decorator).build()); 17. })) 18. .onErrorResume(throwable -> release(exchange, outputMessage, throwable)); 19. } 20. } 20 -
ПОВТОР НЕУСПЕШНОГО ЗАПРОСА 1. @Configuration 2. class WebConfiguration { 3.
4. @Bean 5. fun routers(builder: RouteLocatorBuilder, properties: ApplicationProperties) = 6. builder.routes { 7. route(id = "dictionary") { 8. path("/dict/**") 9. filters { 10. ... 11. retry { 12. it.retries = 3 13. it.setStatuses(BAD_GATEWAY, GATEWAY_TIMEOUT) 14. it.setExceptions(IOException::class.java) 15. it.setBackoff(ofSeconds(1), ofSeconds(10), 2, false) 16. } 17. } 18. uri("http://localhost:8080") 19. } 20. } 21. } 22 -
НАСТРОЙКА SECURITY 1. @Configuration 2. class SecurityConfiguration { 3. 4.
@Bean 5. fun springSecurityFilterChain(http: ServerHttpSecurity) = 6. http.authorizeExchange { it.anyExchange().authenticated() } 7. .httpBasic { } 8. .build() 9. 10. @Bean 11. fun userDetailsService(): ReactiveUserDetailsService { 12. val userDetails = User.withUsername("program") 13. .passwordEncoder { passwordEncoder().encode(it) } 14. .password("test") 15. .authorities("ROLE_USER") 16. .build() 17. return MapReactiveUserDetailsService(userDetails) 18. } 19.} 24 -
НАСТРОЙКА SECURITY $ curl http://localhost:8000/dict/v1/lego-sets/ -v > GET /dict/v1/lego-sets/ HTTP/1.1
> Host: localhost:8000 > < HTTP/1.1 401 Unauthorized < WWW-Authenticate: Basic realm="Realm" $ curl http://localhost:8000/dict/v1/lego-sets/ -v --user ronin:test > GET /dict/v1/lego-sets/ HTTP/1.1 > Host: localhost:8000 > Authorization: Basic cm9uaW46dGVzdA== > < HTTP/1.1 200 OK < Content-Type: application/json 25 -
ЧТО МЫ ПОЛУЧИЛИ? + Единая точка коммуникации frontend-backend. + API
Gateway проверяет ролевую модель и отправляет события аудита, а так же берет на себя ряд утильных функций. + Благодаря этому сократилось время на создание новых сервисов. + Благодаря WebFlux (реактивным потокам) сервер имеет хорошую производительность. + Сокращение времени и издержек на разработку позволило заняться продуктивизацией нашего решения для внешнего рынка. 27