基于用户凭证的API Gateway动态路由

Bobur Umurzokov

Bobur Umurzokov

April 9, 2023

Technology

JWTクレームに基づく動的ルーティング:Apache APISIXとOktaを使用して

動的ルーティングは、ほとんどのモダンなAPIゲートウェイが提供する強力な機能であり、HTTPヘッダー、クエリパラメータ、またはリクエストボディなどのさまざまな基準に基づいて、受信リクエストをリアルタイムで異なるバックエンドサービスにルーティングすることができます。

Apache APISIXの既存の組み込みプラグインを活用することで、開発者はアクセストークン、APIキー、ユーザーIDなどのユーザー認証情報に基づいて動的ルーティングルールを作成することもできます。この記事では、Apache APISIXを使用して認証属性に基づく動的ルーティングを採用する利点を探り、JWTトークンのクレームに基づいてクライアントリクエストを適切なバックエンドサービスに動的にルーティングする方法の設定例を示します。

学習目標

この記事を通じて以下のことを学びます:

  • APIゲートウェイを使用したトラフィックの動的ルーティング。
  • ユーザー認証情報に基づく動的ルーティングが必要な理由。
  • Apache APISIXを使用したJWTトークンのクレームに基づく動的ルーティング。

APIゲートウェイ:トラフィックの動的ルーティング

APIゲートウェイを使用したトラフィックの動的ルーティングは、パフォーマンスの最適化、セキュリティの向上、ユーザーが適切なリソースにアクセスできるようにするために、さまざまなアプリケーションやシナリオで使用できます。

動的ルーティングにより、システムは異なるサーバー間の負荷分散を行うことができます。これにより、利用可能なサービスやサーバーにトラフィックをルーティングすることで高可用性を確保できます。あるサービスやサーバーが失敗した場合、トラフィックは自動的に別の利用可能なサービスやサーバーに再ルーティングされます。

Apache APISIXを使用した動的ルーティング

動的ルーティングは、ユーザーの地理的位置に基づいてトラフィックをルーティングするためにも使用できます。これにより、ユーザーが最も近いサーバーやサービスに接続されるため、応答時間が改善され、レイテンシが減少します。

Apache APISIXを使用した地理的位置に基づくトラフィックルーティング

APIゲートウェイ:ユーザーIDに基づく動的ルーティング

多くの場合、ユーザーが提供するIDに基づいて、特定のサービスやパスにトラフィックをルーティングしたり、ユーザーに関連するデータのみを表示したりしたい場合があります。例えば、マルチテナントアプリケーションでは、異なるテナントが異なるサービスやリソースにアクセスできる場合があります。この場合、APIゲートウェイはユーザー認証情報に基づいて、適切なテナントリソースにのみトラフィックをルーティングできます。または、モバイルアプリケーションでは、デバイスの種類やオペレーティングシステムに基づいて特定のサービスにトラフィックをルーティングできます。

一般的なアプローチの1つは、JWTトークンを使用してAPIへのリクエストを認証および承認することです。これにより、APIゲートウェイでJWTトークンに含まれるクレームを考慮した複雑なルーティングルールを作成し、この情報を使用してリクエストをどこに転送するか、またはどのデータを表示するかを決定できます。このアプローチは、システム内に異なるレベルのアクセス制御を必要とする複数のユーザーがいる場合に特に有用です。

JWTトークンに基づく動的ルーティング

デモ:JWTトークンのクレームに基づく動的ルーティング

このデモでは、カンファレンスセッション、スピーカー、トピック情報を提供するConference APIという既存の公開バックエンドAPIを使用します。実際には、これはあなたのバックエンドサービスであると想定します。ここでは、JWTトークンなどの認証情報を使用してシステムにログインした特定のスピーカーに属するセッションのみをフィルタリングして取得したいとします。例えば、https://conferenceapi.azurewebsites.net/speaker/1/sessionsというリクエストは、特定のスピーカーのセッションのみを表示し、この一意のIDはJWTトークンのクレームの一部としてペイロードに含まれています。以下のデコードされたトークンのペイロード構造を見ると、speakerIdフィールドが含まれています:

カスタムクレームを含むJWTトークン

このシナリオでは、APIゲートウェイの同じRouteにリクエストを送信し、認証ヘッダーから動的URIを計算してリクエストをURIに転送します(以下の図を参照)。これを行うために、Apache APISIX APIゲートウェイレベルでJWTトークンのクレームに基づく動的ルーティングを実装します。これには以下のプラグインを使用します:

  1. openid-connectプラグイン:IDプロバイダー(IdP)と連携し、バックエンドアプリケーションへの未認証リクエストをインターセプトできます。IDプロバイダーとして、カスタムクレームを含むJWTトークンを発行し、JWTトークンを検証するOktaを使用します。他のIdPとしてKeycloakOry Hydraを使用することもできますし、jwt-pluginを使用してJWTトークンを作成し、リクエストを認証および承認することもできます。
  2. serverless-pre-functionプラグイン:カスタムLua関数コードを記述して、リクエストをインターセプトし、JWTトークンのクレームをデコードおよび解析し、クレームの値を新しいカスタムヘッダーに保存して、さらに承認決定を行います。
  3. proxy-rewriteプラグイン:クレームをヘッダーに保存したら、このプラグインを使用してリクエスト転送メカニズムを決定し、Nginxヘッダー変数に基づいて、スピーカー固有のセッションを取得するために使用するURIパスを決定します。この場合、speakerIdが動的に変更され、異なるパス/speaker/$http_speakerId/sessionsが作成されます。プラグインは、Conference APIの関連リソースにリクエストを転送します。

デモ全体で何をカバーするかを理解したら、上記のシナリオを設定し、チュートリアルを完了するための前提条件を確認しましょう。

前提条件

  • Docker:コンテナ化されたetcdとAPISIXをインストールするために使用します。
  • curl:APISIXにリクエストを送信して、ルート、アップストリーム、プラグイン設定を構成するために使用します。Postmanなどの簡単なツールを使用してAPIとやり取りすることもできます。
  • Apache APISIXがターゲット環境にインストールされていること。APISIXは以下のクイックスタートガイドを使用して簡単にインストールおよび起動できます。
  • OKTAアカウントが作成され、新しいアプリが登録されていることを確認してください(このガイドConfiguring 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
  }
}'

プラグイン設定の作成

次に、新しいプラグイン設定オブジェクトを設定します。前述の各プラグインのユースケースに従って、openid-connectserverless-pre-functionproxy-rewriteの3つのプラグインを使用します。curlコマンドを実行する前に、openid-connectプラグインの属性(ClienID、Secret、DiscoveryおよびIntrospectionエンドポイント)をあなたのOktaの詳細に置き換える必要があります。

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

上記の設定で、最も理解が難しい部分は、serverless-pre-functionプラグイン内に記述したLuaのカスタム関数コードかもしれません:

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

基本的に、このプラグインは他の2つのプラグインの前に実行され、以下のことを行います:

  1. AuthorizationヘッダーからJWTトークンを取得します。
  2. JWTトークンから「Bearer 」プレフィックスを削除します。
  3. resty.jwtライブラリを使用してJWTトークンをデコードします。
  4. デコードされたJWTトークンから「speakerId」クレームの値を取得します。
  5. 最後に、「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
}'

上記の設定では、URI /sessionsへのHTTP GETリクエストのみが正しいバックエンドサービスにルーティングされるようにルートマッチングルールを定義しました。

Oktaからトークンを取得する

APISIX側でアップストリーム、プラグイン、ルートを設定した後、speakerIdカスタムクレームを含むトークンをOktaからリクエストします。Oktaを使用してトークンをリクエストするURLを構築するためのガイドに従うか、以下のURLを使用してOktaの発行者とクライアントIDを入力してください:

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トークンが生成されます。

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

他のIDプロバイダーでは、トークンを取得するプロセスが異なる場合があります。

返されたIDトークンを確認するには、値をコピーして任意のJWTデコーダー(例えば、https://token.dev)に貼り付けてください。

動的ルーティングのテスト

最後に、以下の簡単なcurlコマンドを実行して、リクエストが一致する基準とJWTトークンのクレームに基づいて正しいURIパス(スピーカー固有のセッション)にルーティングされていることを確認できます:

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

期待通り、結果が得られました。OktaのJWTクレームでspeakerIdを1に設定すると、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にアクセスできるようにすることができます。
Tags: