{"id":20644985,"url":"https://github.com/romanow/gateway-lecture","last_synced_at":"2025-04-16T02:09:45.084Z","repository":{"id":146005619,"uuid":"530794580","full_name":"Romanow/gateway-lecture","owner":"Romanow","description":"Report and example Spring Cloud Gateway for API Gateway","archived":false,"fork":false,"pushed_at":"2024-09-11T10:28:58.000Z","size":78982,"stargazers_count":4,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-16T02:09:28.522Z","etag":null,"topics":["api-gateway","spring-boot","spring-cloud","spring-cloud-gateway"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Romanow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-30T19:08:59.000Z","updated_at":"2024-12-16T16:04:16.000Z","dependencies_parsed_at":"2024-08-01T12:12:26.189Z","dependency_job_id":"b51d34df-ff25-45d8-921c-9575e9431d75","html_url":"https://github.com/Romanow/gateway-lecture","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fgateway-lecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fgateway-lecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fgateway-lecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fgateway-lecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Romanow","download_url":"https://codeload.github.com/Romanow/gateway-lecture/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249183105,"owners_count":21226142,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-gateway","spring-boot","spring-cloud","spring-cloud-gateway"],"created_at":"2024-11-16T16:18:17.486Z","updated_at":"2025-04-16T02:09:45.066Z","avatar_url":"https://github.com/Romanow.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Как нам Spring Cloud Gateway жить помогает\n\n![Build Workflow](https://github.com/Romanow/gateway-lecture/workflows/Build%20project/badge.svg?branch=master)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)\n\n## Аннотация\n\nНаверняка вы сталкивались с ситуацией, когда на сервисах нужно реализовать какую-то однотипную техническую логику (\nнапример, логирование пользовательских запросов, security и т.п.), каким-то образом модифицировать запрос и пробросить\nего дальше или просто сделать routing из одной точки, а не забивать url всех сервисов на frontend'е?\n\nПоговорим про паттерн API Gateway, и как пример реализации возьмем Spring Cloud Gateway. В режиме Live Coding с его\nпомощью реализуем типовые операции: routing, модификация запросов и ответов, а так же прикрутим к нему security.\n\n## План\n\n1. Разберемся что такое паттерн API Gateway:\n    * какие проблемы он решает;\n    * а для чего его использовать не надо.\n2. В двух словах про Spring Cloud Gateway и WebFlux.\n3. Посмотрим что Spring Cloud Gateway умеет и чем он нам будет полезен:\n    * Настраиваем проксирование запросов.\n    * Добавляем заголовки.\n    * Реализуем Rate Limiter для запросов пользователя.\n    * Логируем запрос и ответ.\n4. Добавляем retry и таймауты на запросы.\n5. Подключаем Spring Cloud Security для защиты наших endpoints.\n\n## Доклад\n\n### Разберемся что такое паттерн API Gateway\n\nШлюз API находится между клиентами и службами, он выполняет функцию обратного прокси, передавая запросы от клиентов к\nсервисам. Также он может выполнять такие специализированные задачи, как аутентификация, SSL-termination и Rate Limiting.\n\nФункции Gateway API можно сгруппировать в соответствии со следующими задачами:\n\n* routing – используется в качестве обратного прокси-сервера для перенаправления запросов на одну или несколько сервисов\n  с помощью маршрутизации L7. Шлюз предоставляет одну конечную точку для клиентов и позволяет разделить клиенты и\n  сервисы;\n* повторное выполнение запросов (retry), Circuit Breaker, timeouts;\n* безопасность – аутентификация и авторизация, black/white list и т.п. (это очень важный пункт, т.к. на API Gateway\n  решается вопрос безопасности, а за ним находится доверенна зона (DMZ));\n* логирование запросов и ответов, мониторинг;\n* кэширование ответов, gzip;\n* агрегация и модификация запросов / ответов.\n\nAPI Gateway в первую очередь утильный элемент системы, который _не должен_ содержать в себе бизнес логики. Но это идет в\nпротиворечие с последним пунктом: \"агрегация и модификация запросов / ответов\", – потому что для любой манипуляции\nданными требуется знать из каких блоков они состоят и как их собирать. А значит на Gateway переносится часть бизнес\nлогики, что делает его связанным с самим сервисом. Этого стоит избегать, но если есть необходимость в таком функционале,\nто использовать его только для \"обогащения\" ответа, а не изменения его структуры.\n\n### В двух словах про Spring Cloud Gateway и WebFlux\n\n### Посмотрим что Spring Cloud Gateway умеет\n\n#### Route Predicate как способ настроить гибкие правила проксирования\n\nНа базе этих правил Spring Cloud Gateway выполняет routing.\n\n[Writing Custom Route Predicate Factories](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#writing-custom-route-predicate-factories)\n\nОсновные правила:\n\n###### Path (Ant-Style формат)\n\n```yaml\nspring:\n    cloud:\n        gateway:\n            routes:\n                -   id: path-route\n                    uri: http://dictionary:8080\n                    predicates:\n                        - Path=/dict/**\n```\n\n[The Path Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-path-route-predicate-factory)\n\n###### Header\n\n```yaml\nspring:\n    cloud:\n        gateway:\n            routes:\n                -   id: header-route\n                    uri: http://dictionary:8080\n                    predicates:\n                        - Header=X-Target-Service, dict\n```\n\n[The Header Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-header-route-predicate-factory)\n\n###### Method\n\n```yaml\nspring:\n    cloud:\n        gateway:\n            routes:\n                -   id: method-route\n                    uri: http://dictionary:8080\n                    predicates:\n                        - Method=GET,POST\n```\n\n[The Method Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-method-route-predicate-factory)\n\n###### Query Params\n\n```yaml\nspring:\n    cloud:\n        gateway:\n            routes:\n                -   id: query-route\n                    uri: http://dictionary:8080\n                    predicates:\n                        - Query=service, dict\n```\n\n[The Query Route Predicate Factory](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-query-route-predicate-factory)\n\n#### CORS\n\n_Cross-Origin Resource Sharing (CORS)_ — механизм, использующий дополнительные HTTP-заголовки, чтобы дать возможность\nагенту пользователя получать разрешения на доступ к выбранным ресурсам с сервера на источнике (домене), отличном от\nтого, что сайт использует в данный момент.\n\nИсточник идентифицируется следующей тройкой параметров: схема, полностью определенное имя хоста и порт.\nНапример, `http://example.com` и `https://example.com` имеют разные источники: первый использует схему `http`, а второй\n`https`. Следовательно, 2 источника отличаются схемой и портом, тогда как хост один и тот же (`example.com`).\n\nТаким образом, если хотя бы один из трех элементов у двух ресурсов отличается, то источник ресурсов также считается\nразным. В кросс-доменный запрос браузер автоматически добавляет заголовок `Origin`, содержащий домен, с которого\nосуществлён запрос. Сервер должен, со своей стороны, ответить специальными заголовками, разрешает ли он такой запрос к\nсебе. Если сервер разрешает кросс-доменный запрос с этого домена – он должен добавить к ответу заголовок\n`Access-Control-Allow-Origin`, содержащий домен запроса или звёздочку `*`.\n\n```yaml\nspring:\n    cloud:\n        gateway:\n            globalcors:\n                cors-configurations:\n                    '[/**]':\n                        allowedOrigins: \"*\"\n                        allowedMethods:\n                            - GET\n                            - POST\n                            - PATCH\n                            - PUT\n                            - DELETE\n```\n\n```shell\n$ curl -X OPTIONS \\\n  -H 'Access-Control-Request-Method: GET' \\\n  -H \"Origin: http://localhost\" \\\n  http://localhost:8000/dict/v1/lego-sets/ -v\n\n* Connected to localhost (127.0.0.1) port 8000 (#0)\n* Server auth using Basic with user 'ronin'\n\u003e OPTIONS /dict/v1/lego-sets/ HTTP/1.1\n\u003e Host: localhost:8000\n\u003e User-Agent: curl/7.79.1\n\u003e Accept: */*\n\u003e Access-Control-Request-Method: GET\n\u003e Origin: http://localhost\n\u003e\n* Mark bundle as not supporting multiuse\n\u003c HTTP/1.1 200 OK\n\u003c Vary: Origin\n\u003c Vary: Access-Control-Request-Method\n\u003c Vary: Access-Control-Request-Headers\n\u003c Access-Control-Allow-Origin: *\n\u003c Access-Control-Allow-Methods: GET,POST,PATCH,PUT,DELETE\n\u003c Cache-Control: no-cache, no-store, max-age=0, must-revalidate\n\u003c Pragma: no-cache\n\u003c Expires: 0\n\u003c X-Content-Type-Options: nosniff\n\u003c X-Frame-Options: DENY\n\u003c X-XSS-Protection: 1 ; mode=block\n\u003c Referrer-Policy: no-referrer\n\u003c content-length: 0\n\u003c\n* Connection #0 to host localhost left intact\n```\n\n#### Gateway Filter Factory как средство модификации запросов\n\n[Writing Custom GatewayFilter Factories](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#writing-custom-gatewayfilter-factories)\n\n##### Модификация path (`StripPrefixGatewayFactory`, `PrefixPathGatewayFilterFactory`)\n\nTarget URL: `http://localhost:8080/api/v1/lego-sets`, Gateway URL: `http://localhost:8080/dict/v1/lego-sets`.\n\n```java\npublic class WebConfiguration {\n\n    @Bean\n    public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {\n        return builder\n            .routes()\n            .route(\"dictionary\", pathSpec -\u003e pathSpec\n                .path(\"/dict/**\")                   // http://localhost:8080/dict/**\n                .filters(filterSpec -\u003e filterSpec\n                    .stripPrefix(1)             // /dict/v1/lego-sets -\u003e /v1/lego-sets\n                    .prefixPath(\"/api\"))        // /v1/lego-sets -\u003e /api/v1/lego-sets\n                .uri(\"http://localhost:8080\"))\n            .build();\n    }\n\n}\n```\n\n##### Добавляем заголовок (`AddRequestHeaderGatewayFilterFactory`)\n\n```java\npublic class WebConfiguration {\n\n    @Bean\n    public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {\n        return builder\n            .routes()\n            .route(\"dictionary\", pathSpec -\u003e pathSpec\n                .path(\"/dict/**\")\n                .filters(filterSpec -\u003e filterSpec\n                    .addRequestHeader(\"X-Gateway-Timestamp\", ISO_DATE_TIME.format(now()))\n                    .uri(routes.getDictionary()))\n                .uri(\"http://localhost:8080\"))\n            .build();\n    }\n\n}\n```\n\n##### Контроль пропускной способности сервиса (`RequestRateLimiterGatewayFilterFactory`)\n\nRate Limiter – ограничение скорости обработки запросов, т.е. это искусственный барьер, который не дает клиенту выполнить\nбольше определенного количества больше операций в единицу времени. Rate Limiter защищает систему от перегрузки, т.е. на\nцелевой сервис попадет только такое количество запросов, которое не приведет к дефициту ресурсов (Resource Starvation).\n\nТак же Rate Limiter может использоваться для предотвращения brute force атак и для контроля доступа к платным ресурсам.\n\nSpring Cloud Gateway предоставляет фильтр `RequestRateLimiterGatewayFilterFactory` для реализации Rate Limiter, но\nоставляет за пользователем выбор ключа (`KeyResolver`) и самого алгоритма (`AbstractRateLimiter`).\n\nДля простоты реализации считать, что Gateway запускаем в один instance или нам не требуется распространять информацию о\nсостоянии buckets между нодами.\n\nДля реализации возьмем алгоритм Token Bucket (Алгоритм маркерной корзины):\n\n![Token Bucket](images/Token%20Bucket.png)\n\nBucket – емкость конечного размера, ассоциированная с пользователем (ip, location и т.п.), куда помещаются маркеры. Если\nколичество маркеров больше заданного объема, то запрос отбрасывается (429 Too Many Requests), иначе в bucket добавляется\ntoken и запрос продолжает выполнение. Количество token в корзине возобновляется в течение времени.\n\n[InMemoryRateLimiter](gateway/src/main/java/ru/romanow/gateway/utils/InMemoryRateLimiter.kt)\n\n```java\n\n@Configuration\npublic class WebConfiguration {\n\n    @Bean\n    public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {\n        return builder\n            .routes()\n            .route(\"dictionary\", pathSpec -\u003e pathSpec\n                .path(\"/dict/**\")\n                .filters(filterSpec -\u003e filterSpec\n                    .requestRateLimiter(rateLimiterConfig -\u003e rateLimiterConfig\n                        .setRateLimiter(rateLimiter())\n                        .setKeyResolver(keyResolver())))\n                .uri(routes.getDictionary()))\n            .build();\n    }\n\n    @Bean\n    public RateLimiter\u003cInMemoryRateLimiter.Config\u003e rateLimiter() {\n        return new InMemoryRateLimiter(1, 2, ofSeconds(10));\n    }\n\n    @Bean\n    public KeyResolver keyResolver() {\n        return exchange -\u003e just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());\n    }\n\n}\n```\n\n[System Design Basics: Rate Limiter](https://medium.com/geekculture/system-design-basics-rate-limiter-351c09a57d14)\n\n##### Повтор запросов (`RetryGatewayFilterFactory`)\n\nЕсли запрос завершился неуспешно (5xx Series), то выполняем повторный запрос. Spring Cloud Gateway позволяет настраивать\nполитику повтора:\n\n* retries – количество повторов;\n* backoff – экспоненциальная задержка перед следующим повтором.\n\n```java\n\n@Configuration\npublic class WebConfiguration {\n\n    @Bean\n    public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {\n        // @formatter:off\n        return builder\n            .routes()\n            .route(\"dictionary\", pathSpec -\u003e pathSpec\n                .path(\"/dict/**\")\n                .filters(filterSpec -\u003e filterSpec\n                    .retry(retryConfig -\u003e retryConfig\n                        .setRetries(3)                                 // 3 повтора\n                        .setStatuses(HttpStatus.NOT_FOUND)             // включить повтор при 404 Not Found\n                        .setSeries(HttpStatus.Series.SERVER_ERROR)     // выполнять повтор при 5xx ошибках\n                        .setBackoff(ofSeconds(1),                      // firstBackoff – первый повтор через 1 секунду,\n                            ofSeconds(10),                     // maxBackoff – не более 10 секунд\n                            2,                                 // factor – коэффициент задержки: backoff = firstBackoff * (factor ^ n), т.е.\n                            false)))                           // basedOnPreviousValue – если true, то backoff = prevBackoff * factor\n\n\n                // если basedOnPreviousValue = true, то prevBackoff * factor\n                .uri(routes.getDictionary()))\n            .build();\n        // @formatter:on\n    }\n\n}\n```\n\n##### Модификация тела ответа (`ModifyResponseBodyGatewayFilterFactory`)\n\n#### Таймауты запросов\n\nЕсли мы знаем, что 99% запросов (99 line) выполняется за 200ms, не имеет смысла ждать окончания выполнения операции\nбольше 400ms. Это называется Fail Fast – если операция выполняется слишком долго, то ее лучше прервать и повторить еще\nраз.\n\nДля задания таймаутов в Java HTTP клиентах можно задать:\n\n* `Connection Timeout` (установка соединения).\n* `Read Timeout` (таймаут чтения из InputStream после установки соединения).\n\nИсточник: [Class URLConnection :: setReadTimeout](https://docs.oracle.com/javase/8/docs/api/java/net/URLConnection.html#setReadTimeout-int-).\n\n```java\n\n@Configuration\npublic class WebConfiguration {\n\n    @Bean\n    @Autowired\n    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {\n        return restTemplateBuilder\n            .setConnectTimeout(Duration.ofSeconds(500))\n            .setReadTimeout(Duration.ofSeconds(500))\n            .build();\n    }\n\n}\n```\n\nЕсли мы используем API Gateway, то мы можем задавать суммарный таймаут на всю операцию. В Spring Cloud Gateway можно\nзадать общий таймаут и отдельный таймаут на каждую операцию:\n\n```java\n\n@Configuration\npublic class WebConfiguration {\n\n    @Bean\n    public RouteLocator routers(RouteLocatorBuilder builder, RoutesProperties routes) {\n        return builder\n            .routes()\n            .route(\"dictionary\", spec -\u003e spec\n                .path(\"/dict/**\")\n                .metadata(RouteMetadataUtils.RESPONSE_TIMEOUT_ATTR, 2000)\n                .uri(routes.getDictionary()))\n            .build();\n    }\n\n}\n```\n\n### Подключаем Spring Cloud Security для защиты наших endpoints\n\nОдна из основных задач API Gateway – авторизация, реализуем ее с помощью `Spring Security`. Т.к. Spring Cloud Gateway\nпостроен на WebFlux, для настройки правил security требуется создать `SecurityWebFilterChain` (отличии от WebMVC, где мы\nнаследовались от `WebSecurityConfigurerAdapter` и задавали конфигурацию `HttpSecurity`).\n\n```kotlin\n@Configuration\nclass SecurityConfiguration {\n\n    @Bean\n    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain =\n        http.authorizeExchange {\n        it.pathMatchers(OPTIONS).permitAll()           // для CORS Preflight Request\n        it.pathMatchers(GET, \"/manage/**\").permitAll() // Actuator Endpoints\n        it.anyExchange().authenticated()               // Все остальное с Basic Auth\n    }\n            .httpBasic { }\n            .build()\n}\n```\n\n### Дополнительные возможности\n\nМожно посмотреть настройки проксирования и примененные правила:\n\n```shell\n$ curl http://localhost:8000/manage/gateway/routes | jq\n```\n\nПо конкретному route:\n\n```shell\n$ curl http://localhost:8000/manage/gateway/routes/dictionary | jq\n```\n\n## Запуск примера\n\n```shell\n# запускает postgres и dictionary\n$ docker compose up -d --wait\n$ ./gradlew gateway:bootRun --args='--spring.profiles.active=local'\n\n```\n\n## Ссылки\n\n1. [Spring Cloud Gateway](https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html)\n2. [Bucket4j Rate Limiter](https://github.com/bucket4j/bucket4j)\n\n## TODO\n\n1. Описать проблематику: какие сложности у нас будут без Gateway.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromanow%2Fgateway-lecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fromanow%2Fgateway-lecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromanow%2Fgateway-lecture/lists"}