Dynamisches Routing basierend auf Benutzeranmeldeinformationen mit API Gateway

Bobur Umurzokov

Bobur Umurzokov

April 9, 2023

Technology

Dynamisches Routing basierend auf JWT-Claims mit Apache APISIX und Okta

Dynamisches Routing ist eine leistungsstarke Funktion der meisten modernen API-Gateways, die es ermöglicht, eingehende Anfragen in Echtzeit basierend auf verschiedenen Kriterien wie HTTP-Headern, Abfrageparametern oder sogar dem Anfragekörper an verschiedene Backend-Dienste weiterzuleiten.

Durch die Nutzung der vorhandenen integrierten Plugins von Apache APISIX können Entwickler auch dynamische Routing-Regeln erstellen, die auf verschiedenen Benutzeranmeldeinformationen wie Zugriffstoken, API-Schlüsseln oder Benutzer-IDs basieren. In diesem Artikel untersuchen wir die Vorteile der Einführung von dynamischem Routing basierend auf Authentifizierungsattributen mit Apache APISIX und zeigen Ihnen eine Beispielkonfiguration, wie Client-Anfragen dynamisch an die verantwortlichen Backend-Dienste basierend auf dem JWT-Token-Claim weitergeleitet werden können.

Lernziele

Im Laufe des Artikels werden Sie Folgendes lernen:

  • Dynamisches Routing von Datenverkehr mit einem API-Gateway.
  • Warum benötigen wir dynamisches Routing basierend auf Benutzeranmeldeinformationen?
  • Dynamisches Routing basierend auf JWT-Token-Claims mit Apache APISIX.

API-Gateway: Dynamisches Routing von Datenverkehr

Dynamisches Routing von Datenverkehr mit dem API-Gateway kann in einer Vielzahl von Anwendungen und Szenarien verwendet werden, um die Leistung zu optimieren, die Sicherheit zu verbessern und sicherzustellen, dass Benutzer Zugriff auf die entsprechenden Ressourcen haben.

Durch dynamisches Routing kann ein System die Last zwischen verschiedenen Servern oder Diensten ausgleichen. Es kann helfen, hohe Verfügbarkeit sicherzustellen, indem der Datenverkehr an verfügbare Dienste oder Server weitergeleitet wird. Wenn ein Dienst oder Server ausfällt, kann der Datenverkehr automatisch an einen anderen verfügbaren Dienst oder Server umgeleitet werden.

Dynamisches Routing von Datenverkehr mit Apache APISIX

Dynamisches Routing kann auch verwendet werden, um den Datenverkehr basierend auf der Geolokation des Benutzers weiterzuleiten. Dies kann sicherstellen, dass Benutzer mit dem nächstgelegenen Server oder Dienst verbunden werden, was die Antwortzeiten verbessert und die Latenz verringert.

Geolokationsbasiertes Routing von Datenverkehr mit Apache APISIX

API-Gateway: Benutzeridentitätsbasiertes dynamisches Routing

Oft möchten wir den Datenverkehr basierend auf der vom Benutzer bereitgestellten Identität an bestimmte Dienste, Pfade weiterleiten oder nur Daten anzeigen, die mit dem Benutzer zusammenhängen. Zum Beispiel können in Multi-Tenant-Anwendungen verschiedene Mieter Zugriff auf verschiedene Dienste oder Ressourcen haben. In diesem Fall kann das API-Gateway den Datenverkehr nur an die entsprechenden Mieterressourcen basierend auf den Benutzeranmeldeinformationen weiterleiten. Oder in mobilen Anwendungen kann es den Datenverkehr basierend auf dem Gerätetyp oder Betriebssystem an bestimmte Dienste weiterleiten.

Ein gängiger Ansatz ist die Verwendung von JWT-Token zur Authentifizierung und Autorisierung von Anfragen an APIs. Dies bedeutet, dass wir komplexe Routing-Regeln mit dem API-Gateway erstellen können, die die Claims im JWT-Token berücksichtigen und diese Informationen verwenden, um zu entscheiden, wohin die Anfrage weitergeleitet werden soll oder welche Daten angezeigt werden sollen. Dieser Ansatz ist besonders nützlich, wenn Sie mehrere Benutzer im System haben, die unterschiedliche Zugriffskontrollen erfordern.

Dynamisches Routing von Datenverkehr basierend auf JWT-Token mit Apache APISIX

Demo: Dynamisches Routing basierend auf JWT-Token-Claims

In dieser Demo verwenden wir die vorhandene öffentliche Backend-API namens Conference API mit Konferenzsitzungen, Sprechern und Themeninformationen. In der Realität kann dies Ihr Backend-Dienst sein. Nehmen wir an, wir möchten nur Sitzungen filtern und abrufen, die einem bestimmten Sprecher gehören, der sich mit seinen Anmeldeinformationen wie einem JWT-Token in das System eingeloggt hat. Zum Beispiel: https://conferenceapi.azurewebsites.net/speaker/1/sessions

Die Anfrage zeigt nur Sitzungen eines Sprechers mit einer eindeutigen ID, und diese eindeutige ID stammt aus dem JWT-Token-Claim als Teil seiner Nutzdaten. Schauen Sie sich die folgende decodierte Token-Nutzdatenstruktur an, es ist auch ein speakerId-Feld enthalten:

JWT-Token mit einem benutzerdefinierten Claim

In diesem Szenario senden wir Anfragen an dieselbe Route beim API-Gateway, und es berechnet die dynamische URI aus dem Autorisierungsheader und leitet die Anfrage an die URI weiter (siehe untenstehendes Diagramm, um den Ablauf zu verstehen). Dazu implementieren wir ein dynamisches Routing auf der Ebene des Apache APISIX API-Gateways basierend auf dem JWT-Token-Claim durch die Verwendung der folgenden Plugins:

  1. openid-connect Plugin, das mit dem Identitätsanbieter (IdP) interagiert und nicht authentifizierte Anfragen rechtzeitig an Backend-Anwendungen abfangen kann. Als Identitätsanbieter verwenden wir Okta, das ein JWT-Token mit unserem benutzerdefinierten Claim ausstellt und das JWT-Token validiert. Oder Sie können andere IdPs wie Keycloak, Ory Hydra verwenden oder sogar das jwt-plugin verwenden, um ein JWT-Token zu erstellen und Anfragen zu authentifizieren und zu autorisieren.
  2. serverless-pre-function Plugin, um einen benutzerdefinierten Lua-Funktionscode zu schreiben, der die Anfrage abfängt, den JWT-Token-Claim decodiert und analysiert und den Wert des Claims in einem neuen benutzerdefinierten Header speichert, um weitere Autorisierungsentscheidungen zu treffen.
  3. proxy-rewrite Plugin, sobald wir den Claim im Header haben, verwenden wir dieses Plugin als Mechanismus zur Anfrageweiterleitung, um zu bestimmen, welcher URI-Pfad basierend auf der Nginx-Header-Variable in unserem Fall speakerId verwendet werden muss, der sich dynamisch ändert, um verschiedene Pfade /speaker/$http_speakerId/sessions zu erstellen. Das Plugin leitet die Anfrage an die entsprechende Ressource in der Conference API weiter.

Sobald wir verstanden haben, was wir in der Demo behandeln werden, lassen wir uns die Voraussetzungen überprüfen, um mit der Konfiguration des oben beschriebenen Szenarios zu beginnen und das Tutorial abzuschließen.

Voraussetzungen

Konfigurieren des Backend-Dienstes (Upstream)

Sie müssen den Backend-Dienst für die Conference API konfigurieren, an den Sie Anfragen weiterleiten möchten. Dies kann durch Hinzufügen eines Upstream-Servers in Apache APISIX über die Admin API erfolgen.

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
  }
}'

Erstellen einer Plugin-Konfiguration

Als Nächstes richten wir ein neues Plugin-Konfigurationsobjekt ein. Wir verwenden 3 Plugins: openid-connect, serverless-pre-function und proxy-rewrite, wie wir die Anwendungsfälle jedes Plugins zuvor besprochen haben. Sie müssen nur die openid-connect-Plugin-Attribute (ClientID, Secret, Discovery und Introspection Endpoints) durch Ihre eigenen Okta-Details ersetzen, bevor Sie den curl-Befehl ausführen.

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)

    -- Import neccessary libraries
    local core = require(\"apisix.core\")
    local jwt = require(\"resty.jwt\")

    -- Retrieve the JWT token from the Authorization header
    local jwt_token = core.request.header(ctx, \"Authorization\")
    if jwt_token ~= nil then
        -- Remove the Bearer prefix from the JWT token
        local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
        if jwt_token_only ~= nil then
           -- Decode the JWT token
           local jwt_obj = jwt:load_jwt(jwt_token_only)

           if jwt_obj.valid then
             -- Retrieve the value of the speakerId claim from the JWT token
             local speakerId_claim_value = jwt_obj.payload.speakerId

             -- Store the speakerId claim value in the header variable
             core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
           end
         end
     end
   end
    "]}
    }
}'

In der obigen Konfiguration kann der schwierigste Teil der benutzerdefinierte Funktionscode sein, den wir in Lua innerhalb des serverless-pre-function-Plugins geschrieben haben:

return function(conf, ctx)
    -- Import neccessary libraries
    local core = require(\"apisix.core\")
    local jwt = require(\"resty.jwt\")

    -- Retrieve the JWT token from the Authorization header
    local jwt_token = core.request.header(ctx, \"Authorization\")
    if jwt_token ~= nil then
        -- Remove the Bearer prefix from the JWT token
        local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
        if jwt_token_only ~= nil then
           -- Decode the JWT token
           local jwt_obj = jwt:load_jwt(jwt_token_only)

           if jwt_obj.valid then
             -- Retrieve the value of the speakerId claim from the JWT token
             local speakerId_claim_value = jwt_obj.payload.speakerId

             -- Store the speakerId claim value in the header variable
             core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
           end
         end
   end
end

Grundsätzlich wird dieses Plugin vor den anderen beiden Plugins ausgeführt und führt Folgendes aus:

  1. Ruft das JWT-Token aus dem Authorization-Header ab.
  2. Entfernt das "Bearer "-Präfix aus dem JWT-Token.
  3. Decodiert das JWT-Token mit der resty.jwt-Bibliothek.
  4. Ruft den Wert des "speakerId"-Claims aus dem decodierten JWT-Token ab.
  5. Speichert schließlich den Wert des "speakerId"-Claims in der speakerId-Header-Variable.

Konfigurieren einer neuen Route

Dieser Schritt beinhaltet das Einrichten einer neuen Route, die die Plugin-Konfiguration verwendet, und das Konfigurieren der Route, um mit dem Upstream (durch Referenzierung ihrer IDs) zu arbeiten, den wir in den vorherigen Schritten erstellt haben:

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
}'

In der obigen Konfiguration haben wir die Routenabgleichsregeln definiert, sodass nur HTTP-GET-Anfragen an die URI /sessions an den richtigen Backend-Dienst weitergeleitet werden.

Token von Okta abrufen

Nachdem wir den Upstream, die Plugins und die Route auf der APISIX-Seite konfiguriert haben, fordern wir nun ein Token von Okta an, das unseren benutzerdefinierten Claim speakerId enthält. Sie können dem Leitfaden folgen, der Informationen zum Erstellen einer URL zum Anfordern eines Tokens mit Okta enthält, oder einfach die folgende resultierende URL mit Ihrem Okta-Issuer und Ihrer Client-ID verwenden:

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

Nachdem Sie die Anfrage in Ihren Browser eingefügt haben, wird der Browser zur Anmeldeseite Ihres Okta weitergeleitet und generiert ein ID-Token.

https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}

Beachten Sie, dass der Prozess zum Abrufen eines Tokens bei anderen Identitätsanbietern unterschiedlich sein kann.

Um das zurückgegebene ID-Token zu überprüfen, können Sie den Wert kopieren und in einen beliebigen JWT-Decoder einfügen (zum Beispiel https://token.dev).

Testen des dynamischen Routings

Schließlich können wir jetzt überprüfen, ob die Anfrage basierend auf den Abgleichkriterien und dem JWT-Token-Claim an den richtigen URI-Pfad (mit sprecher-spezifischen Sitzungen) weitergeleitet wird, indem wir einen weiteren einfachen curl-Befehl ausführen:

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}"

Hier ist das Ergebnis, wie wir es erwartet haben. Wenn wir speakerId auf 1 im Okta-JWT-Claim setzen, hat Apisix die Anfrage an den relevanten URI-Pfad weitergeleitet und alle Sitzungen dieses Sprechers in der Antwort zurückgegeben.

{
  "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"
          }
        ],
      }
    ]
   }
}

Erkenntnisse

  • Mit einem API-Gateway können Sie den Datenverkehr basierend auf verschiedenen Kriterien an verschiedene Backend-Dienste weiterleiten.
  • Dynamisches Routing kann abhängig von Benutzerattributen, die im Anfrageheader, in der Abfrage oder im Körper angegeben sind, erreicht werden.
  • Sie können komplexe Routing-Regeln erstellen, die die Claims im JWT-Token berücksichtigen und sicherstellen, dass nur autorisierte Anfragen Zugriff auf Ihre API haben.
Tags: