Динамическая маршрутизация на основе учетных данных пользователя с использованием API Gateway
April 9, 2023
Динамическая маршрутизация на основе JWT Claim с использованием Apache APISIX и Okta
Динамическая маршрутизация — это мощная функция большинства современных API-шлюзов, которая позволяет маршрутизировать входящие запросы в реальном времени на различные серверные службы на основе различных критериев, таких как HTTP-заголовки, параметры запроса или даже тело запроса.
Используя встроенные плагины Apache APISIX, разработчики также могут создавать правила динамической маршрутизации, основанные на различных учетных данных пользователя, таких как токены доступа, API-ключи или идентификаторы пользователей. В этой статье мы рассмотрим преимущества использования динамической маршрутизации на основе атрибутов аутентификации с Apache APISIX и покажем пример конфигурации динамической маршрутизации клиентских запросов на соответствующие серверные службы на основе утверждения (claim) в JWT токене.
Цели обучения
В этой статье вы узнаете следующее:
- Динамическая маршрутизация трафика с использованием API-шлюза.
- Зачем нужна динамическая маршрутизация на основе учетных данных пользователя?
- Динамическая маршрутизация на основе утверждений JWT токена с использованием Apache APISIX.
API-шлюз: Динамическая маршрутизация трафика
Динамическая маршрутизация трафика с использованием API-шлюза может применяться в широком спектре приложений и сценариев для оптимизации производительности, повышения безопасности и обеспечения доступа пользователей к соответствующим ресурсам.
С помощью динамической маршрутизации система может балансировать нагрузку между различными серверами или службами. Это помогает обеспечить высокую доступность, маршрутизируя трафик на доступные службы или серверы. Если одна служба или сервер выходит из строя, трафик может быть автоматически перенаправлен на другую доступную службу или сервер.

Динамическая маршрутизация также может использоваться для маршрутизации трафика на основе геолокации пользователя. Это помогает обеспечить подключение пользователей к ближайшему серверу или службе, улучшая время отклика и снижая задержку.

API-шлюз: Динамическая маршрутизация на основе идентификатора пользователя
Часто мы хотим маршрутизировать трафик на определенные службы, пути или показывать только данные, связанные с пользователем, на основе предоставленного пользователем идентификатора. Например, в многопользовательских приложениях разные пользователи могут иметь доступ к разным службам или ресурсам. В этом случае API-шлюз может маршрутизировать трафик только на соответствующие ресурсы пользователя на основе его учетных данных. Или в мобильных приложениях он может маршрутизировать трафик на определенные службы в зависимости от типа устройства или операционной системы.
Одним из распространенных подходов является использование JWT токенов для аутентификации и авторизации запросов к API. Это означает, что мы можем создавать сложные правила маршрутизации с помощью API-шлюза, которые учитывают утверждения, присутствующие в JWT токене, и используют эту информацию для принятия решения о том, куда перенаправить запрос или какие данные показать. Этот подход особенно полезен, когда в системе есть несколько пользователей, которым требуются разные уровни контроля доступа.

Демонстрация: Динамическая маршрутизация на основе утверждений JWT токена
В этой демонстрации мы используем существующий публичный API под названием Conference API, который предоставляет информацию о сессиях, докладчиках и темах конференции. В реальности это может быть ваш серверный сервис. Предположим, что мы хотим фильтровать и получать только сессии, принадлежащие конкретному докладчику, который вошел в систему, используя свои учетные данные, такие как JWT токен. Например, https://conferenceapi.azurewebsites.net/speaker/1/sessions
запрос показывает только сессии докладчика с уникальным идентификатором, и этот уникальный идентификатор берется из утверждения JWT токена как часть его полезной нагрузки. Взгляните на структуру декодированной полезной нагрузки токена ниже, в ней также включено поле speakerId:

В этом сценарии мы отправляем запросы на один и тот же маршрут в API-шлюзе, и он вычисляет динамический URI из заголовка авторизации и перенаправляет запрос на этот URI (см. диаграмму ниже, чтобы понять поток). Для этого мы собираемся реализовать динамическую маршрутизацию на уровне API-шлюза Apache APISIX на основе утверждения JWT токена с использованием следующих плагинов:
- Плагин openid-connect, который взаимодействует с поставщиком идентификации (IdP) и может перехватывать неаутентифицированные запросы до их передачи в серверные приложения. В качестве поставщика идентификации мы используем Okta, который выдает JWT токен с нашим пользовательским утверждением и проверяет JWT токен. Или вы можете использовать другие IdP, такие как Keycloak, Ory Hydra, или даже использовать jwt-plugin для создания JWT токена, аутентификации и авторизации запросов.
- Плагин serverless-pre-function для написания пользовательского кода на Lua, который перехватывает запрос, декодирует и анализирует утверждение JWT токена и сохраняет значение утверждения в новом пользовательском заголовке для дальнейшего принятия решений по авторизации.
- Плагин proxy-rewrite, как только у нас есть утверждение в заголовке, мы используем этот плагин как механизм перенаправления запроса для определения, какой URI путь должен быть использован для получения сессий конкретного докладчика на основе переменной заголовка Nginx, в нашем случае это
speakerId, который динамически изменяется для создания различных путей/speaker/$http_speakerId/sessions. Плагин перенаправит запрос на соответствующий ресурс в Conference API.
Как только мы поняли, что собираемся рассмотреть в демонстрации, давайте проверим предварительные условия для начала настройки вышеуказанного сценария и завершения руководства.
Предварительные условия
- Docker используется для установки контейнеризованного etcd и APISIX.
- curl используется для отправки запросов в APISIX для настройки маршрутов, апстримов и конфигураций плагинов. Вы также можете использовать удобные инструменты, такие как Postman, для взаимодействия с API.
- Apache APISIX установлен в вашей целевой среде. APISIX можно легко установить и запустить, следуя краткому руководству.
- Убедитесь, что ваш аккаунт OKTA создан, вы зарегистрировали новое приложение (вы можете следовать этому руководству Настройка Okta), добавили пользовательское утверждение в токен с помощью панели управления Okta и запросили токен, содержащий пользовательское утверждение под названием
speakerId.
Настройка серверной службы (апстрим)
Вам нужно настроить серверную службу для Conference API, на которую вы хотите маршрутизировать запросы. Это можно сделать, добавив апстрим-сервер в Apache APISIX через Admin API.
curl "http://127.0.0.1:9180/apisix/admin/upstreams/1" -X PUT -d ' { "name": "Conferences API upstream", "desc": "Register Conferences API as the upstream", "type": "roundrobin", "scheme": "https", "nodes": { "conferenceapi.azurewebsites.net:443": 1 } }'
Создание конфигурации плагина
Далее мы настраиваем новый объект конфигурации плагина. Мы будем использовать 3 плагина: openid-connect, serverless-pre-function и proxy-rewrite, как мы обсуждали ранее. Вам нужно заменить только атрибуты плагина openid-connect (ClienID, Secret, Discovery и Introspection endpoints) на ваши собственные данные Okta перед выполнением команды curl.
curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d ' { "plugins": { "openid-connect":{ "client_id":"{YOUR_OKTA_CLIENT_ID}", "client_secret":"{YOUR_OKTA_CLIENT_SECRET}", "discovery":"https://{YOUR_OKTA_ISSUER}/oauth2/default/.well-known/openid-configuration", "scope":"openid", "bearer_only":true, "realm":"master", "introspection_endpoint_auth_method":"https://{YOUR_OKTA_ISSUER}/oauth2/v1/introspect", "redirect_uri":"https://conferenceapi.azurewebsites.net/" }, "proxy-rewrite": { "uri": "/speaker/$http_speakerId/sessions", "host":"conferenceapi.azurewebsites.net" }, "serverless-pre-function": { "phase": "rewrite", "functions" : ["return function(conf, ctx) -- Импорт необходимых библиотек local core = require(\"apisix.core\") local jwt = require(\"resty.jwt\") -- Получение JWT токена из заголовка Authorization local jwt_token = core.request.header(ctx, \"Authorization\") if jwt_token ~= nil then -- Удаление префикса Bearer из JWT токена local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\") if jwt_token_only ~= nil then -- Декодирование JWT токена local jwt_obj = jwt:load_jwt(jwt_token_only) if jwt_obj.valid then -- Получение значения утверждения speakerId из JWT токена local speakerId_claim_value = jwt_obj.payload.speakerId -- Сохранение значения утверждения speakerId в переменной заголовка core.request.set_header(ctx, \"speakerId\", speakerId_claim_value) end end end end "]} } }'
В приведенной выше конфигурации самой сложной для понимания частью может быть пользовательский код на Lua, который мы написали внутри плагина serverless-pre-function:
return function(conf, ctx) -- Импорт необходимых библиотек local core = require(\"apisix.core\") local jwt = require(\"resty.jwt\") -- Получение JWT токена из заголовка Authorization local jwt_token = core.request.header(ctx, \"Authorization\") if jwt_token ~= nil then -- Удаление префикса Bearer из JWT токена local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\") if jwt_token_only ~= nil then -- Декодирование JWT токена local jwt_obj = jwt:load_jwt(jwt_token_only) if jwt_obj.valid then -- Получение значения утверждения speakerId из JWT токена local speakerId_claim_value = jwt_obj.payload.speakerId -- Сохранение значения утверждения speakerId в переменной заголовка core.request.set_header(ctx, \"speakerId\", speakerId_claim_value) end end end end
По сути, этот плагин будет выполнен перед двумя другими плагинами и выполняет следующее:
- Получает JWT токен из заголовка Authorization.
- Удаляет префикс "Bearer " из JWT токена.
- Декодирует JWT токен с использованием библиотеки resty.jwt.
- Получает значение утверждения "speakerId" из декодированного JWT токена.
- Наконец, сохраняет значение утверждения "speakerId" в переменной заголовка speakerId.
Настройка нового маршрута
Этот шаг включает настройку нового маршрута, который использует конфигурацию плагина, и настройку маршрута для работы с апстримом (ссылаясь на их ID), который мы создали на предыдущих шагах:
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "name":"Conferences API speaker sessions route", "desc":"Create a new route in APISIX for the Conferences API speaker sessions", "methods": ["GET"], "uri": "/sessions", "upstream_id":"1", "plugin_config_id":1 }'
В приведенной выше конфигурации мы определили правила сопоставления маршрута, такие как только HTTP GET запросы к URI /sessions будут маршрутизированы на правильный серверный сервис.
Получение токена от Okta
После настройки апстрима, плагинов и маршрута на стороне APISIX, теперь мы запрашиваем токен от Okta, который содержит наше пользовательское утверждение speakerId. Вы можете следовать руководству, которое включает информацию о построении URL для запроса токена с Okta, или просто использовать следующий URL с вашим издателем Okta и идентификатором клиента:
https://{YOUR_OKTA_ISSUER}/oauth2/default/v1/authorize?client_id={YOUR_OKTA_CLIENT_ID} &response_type=id_token &scope=openid &redirect_uri=https%3A%2F%2Fconferenceapi.azurewebsites.net &state=myState &nonce=myNonceValue
После того как вы вставите запрос в браузер, браузер перенаправит вас на страницу входа в вашу учетную запись Okta и сгенерирует ID Token.
https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}
Обратите внимание, что процесс получения токена может отличаться для других поставщиков идентификации.
Чтобы проверить возвращенный ID токен, вы можете скопировать значение и вставить его в любой декодер JWT (например, https://token.dev).
Тестирование динамической маршрутизации
Наконец, теперь мы можем проверить, что запрос маршрутизируется на правильный URI путь (с сессиями конкретного докладчика) на основе критериев сопоставления и утверждения JWT токена, выполнив еще одну простую команду curl:
curl -i -X "GET [http://127.0.0.1:9080/sessions](http://127.0.0.1:9080/sessions)" -H "Authorization: Bearer {YOUR_OKTA_JWT_TOKEN}"
Вот и все, результат, как мы и ожидали. Если мы установим speakerId равным 1 в утверждении JWT токена Okta, Apisix перенаправил запрос на соответствующий URI путь и вернул все сессии этого докладчика в ответе.
{ "collection": { "version": "1.0", "links": [], "items": [ { "href": "https://conferenceapi.azurewebsites.net/session/114", "data": [ { "name": "Title", "value": "\r\n\t\t\tIntroduction to Windows Azure Part I\r\n\t\t" }, { "name": "Timeslot", "value": "04 December 2013 13:40 - 14:40" }, { "name": "Speaker", "value": "Scott Guthrie" } ], "links": [ { "rel": "http://tavis.net/rels/speaker", "href": "https://conferenceapi.azurewebsites.net/speaker/1" }, { "rel": "http://tavis.net/rels/topics", "href": "https://conferenceapi.azurewebsites.net/session/114/topics" } ] }, { "href": "https://conferenceapi.azurewebsites.net/session/121", "data": [ { "name": "Title", "value": "\r\n\t\t\tIntroduction to Windows Azure Part II\r\n\t\t" }, { "name": "Timeslot", "value": "04 December 2013 15:00 - 16:00" }, { "name": "Speaker", "value": "Scott Guthrie" } ], } ] } }
Основные выводы
- С помощью API-шлюза вы можете маршрутизировать трафик на различные серверные службы на основе различных критериев.
- Динамическая маршрутизация может быть достигнута в зависимости от атрибутов пользователя, указанных в заголовке запроса, запросе или теле.
- Вы можете создавать сложные правила маршрутизации, которые учитывают утверждения, присутствующие в JWT токене, и гарантировать, что только авторизованные запросы могут получить доступ к вашему API.