Alibaba Cloud API Gateway は、JSON Web トークン (JWT) を使用して、お客様独自のユーザーシステムに基づいて API アクセスを認可し、カスタムセキュリティを設定できます。
1. トークンベースの認証
概要
多くのパブリック API では、要求されたリソースへのアクセスを許可するかどうかを判断するために、呼び出し元を識別する必要があります。トークンは認証のためのメカニズムです。トークンベースの認証を使用すると、アプリケーションはサーバーにユーザー認証情報やセッション情報を保存する必要がなくなり、ステートレスで分散型の Web アプリケーションの認可が可能になり、アプリケーションのスケーリングが簡素化されます。
ワークフロー
API Gateway は、JWT 認証プラグインを使用して認証を処理します。ワークフローは次のとおりです。
-
クライアントは、トークンを含むリクエストを API Gateway に送信します。
-
API Gateway は、JWT 認証プラグインに設定された公開鍵を使用してトークンを検証します。トークンが有効な場合、API Gateway はリクエストをバックエンドサービスに渡します。
-
バックエンドサービスはリクエストを処理し、レスポンスを返します。
-
API Gateway は、バックエンドサービスからのレスポンスをクライアントに返します。
このプロセス全体を通じて、API Gateway はトークン認証を使用し、お客様独自のユーザーシステムに基づいて API アクセスを認可します。以降のセクションでは、API Gateway が認証に使用する JSON Web トークン (JWT) について説明します。
JWT
1.1 概要
JSON Web トークン (JWT) は、Web アプリケーション環境のパーティ間でクレームを安全に送信するための、JSON ベースのオープンスタンダード (RFC 7519) です。JWT は、ユーザー ID、ロール、権限などを含むことができる自己完結型の認証トークンとして機能します。また、ビジネスロジックで必要とされる追加のクレームを含めることもできるため、JWT は分散アプリケーションにおける認証に最適です。
1.2 JWT の構造
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
上記の例に示すように、JWT は次の 3 つの部分で構成される文字列です。
-
ヘッダー
-
ペイロード
-
署名
ヘッダー
ヘッダーには 2 つの部分が含まれます。
-
トークンのタイプ (JWT)
-
使用される署名アルゴリズム
完全なヘッダーは、次の例に示すような JSON オブジェクトです。
{
'typ': 'JWT',
'alg': 'HS256'
}
次に、ヘッダーを Base64Url でエンコードして、JWT の最初の部分を作成します。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
ペイロード
ペイロードにはクレームが含まれます。
iss:発行者。トークンを発行したプリンシパル。このクレームは文字列です。
sub:サブジェクト。トークンのサブジェクトであるプリンシパル。この値は、発行者のスコープ内で一意です。大文字と小文字を区別する文字列で、最大長は 255 ASCII 文字です。
aud:オーディエンス。トークンの対象となる受信者。これは、大文字と小文字を区別する文字列の配列です。
exp:有効期限。この時刻を過ぎるとトークンは無効と見なされます。このクレームは、1970-01-01T00:00:00Z からの秒数を表す整数です。
iat:発行日時。トークンが発行された時刻。このクレームは、1970-01-01T00:00:00Z からの秒数を表す整数です。
jti:JWT ID。トークンの一意の識別子。この値は、同じ発行者によって作成された各トークンに対して一意であり、衝突を避けるために暗号学的にランダムな値であることがよくあります。この値は、攻撃者が取得できないランダムなエントロピーコンポーネントを構造化トークンに追加し、トークンの推測やリプレイ攻撃を防ぐのに役立ちます。
また、ユーザーシステムで必要となるカスタムクレームを追加することもできます。たとえば、ユーザーのニックネームとして name クレームを追加できます。
{
"sub": "1234567890",
"name": "John Doe"
}
次に、ペイロードを Base64Url でエンコードして、JWT の 2 番目の部分を作成します。
JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE
署名
署名を作成するには、Base64Url でエンコードされたヘッダーとペイロードをピリオド (.) で結合します。次に、ヘッダーで指定されたアルゴリズムと秘密鍵 ($secret) を使用して、結果の文字列に署名します。これにより、JWT の 3 番目の部分が作成されます。
// JavaScript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');
3 つの部分をピリオド (.) で結合して、最初の JWT の例で示したような完全な JWT を作成します。
1.3 認可スコープと有効期間
API Gateway は、API グループ内の JWT プラグインにバインドされているすべての API へのアクセスを認可されたトークンと見なします。よりきめ細かいアクセス制御を行うには、バックエンドサービスでトークンを解析し、認可を実行する必要があります。API Gateway はトークン内の exp フィールドを検証します。トークンが期限切れの場合、API Gateway はリクエストを直ちに拒否します。有効期限を設定する必要があり、その値は 7 日未満でなければなりません。
1.4 JWT の主な特徴
-
デフォルトでは、JWT は暗号化されません。JWT に機密データを含めないでください。
-
JWT は認証と情報交換の両方に使用できるため、サーバーでのデータベースクエリの回数を削減できます。主な欠点は、ステートレスである点です。トークンの有効期限が切れる前に、トークンを失効させたり、その権限を変更したりすることはできません。一度発行されると、サーバーがカスタムの失効ロジックを実装しない限り、JWT は有効期限が切れるまで有効なままです。
-
JWT には認証情報が含まれています。漏洩した場合、トークンを取得した人は誰でも、関連するすべての権限を得ます。このリスクを軽減するには、JWT の有効期間を短く設定してください。セキュリティレベルの高いアクションの場合は、アクセスを許可する前にユーザーを再認証してください。
-
盗難のリスクを軽減するため、JWT を HTTP 経由で平文で送信しないでください。代わりに HTTPS を使用してください。
2. JWT プラグインによる API の保護
2.1 JSON Web キー (JWK) ペアの生成
2.1.1 オンラインでのキーペアの生成
https://tools.top/jwt-encode.html にアクセスして、トークンの生成と検証のための秘密鍵と公開鍵を生成できます。秘密鍵は認可サービスが JWT を発行するために使用し、公開鍵は API Gateway がリクエストの署名を検証するために JWT 認証プラグインに設定します。API Gateway は、RSA SHA256 アルゴリズムと 2048 ビットのキーサイズをサポートしています。
サイトを開いたら、[JWK format] タブを選択します。ページが自動的にキーペアを生成します。[Copy Public Key] または [Copy Private Key] をクリックして、対応する JWK の内容を取得します。
2.1.2 ローカルでのキーペアの生成
次の例では Java を使用します。他のプログラミング言語でも、キーペアを生成するための同様のツールを見つけることができます。Maven プロジェクトを作成し、次の依存関係を追加します。
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.0</version>
</dependency>
次のコードを使用して RSA キーペアを生成します。
RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("uniq_key");
final String publicKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
final String privateKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);
2.2 トークン発行サービスの実装
「セクション 2.1」で生成された秘密鍵の JWK JSON 文字列 (オンラインで生成されたもの、またはローカルで生成された privateKeyString JSON 文字列) を使用して、トークンを発行します。これらのトークンは、信頼できるユーザーが保護された API にアクセスすることを認可します。詳細については、「トークン発行サービスのサンプルコード」をご参照ください。トークンの発行方法は、ビジネスシナリオによって異なります。トークン発行機能を本番環境に通常の API としてデプロイし、訪問者がユーザー名とパスワードでトークンを取得できるようにすることも、ローカルでトークンを生成して特定のユーザーに直接提供することもできます。
2.3 JWT プラグインの設定
-
API Gateway コンソールにログインします。
-
左側のナビゲーションペインで、 を選択します。
-
プラグイン管理ページで、右上の [プラグインの作成] をクリックします。
-
[プラグインの作成] ページで、[プラグイン名] を設定し、[プラグインタイプ] を選択します。次のコードは、JWT 認証プラグインの設定例です。設定の詳細については、「JWT 認証プラグイン」をご参照ください。
---
parameter: X-Token # JWT を取得するパラメーター。これは API パラメーターに対応します。
parameterLocation: header # JWT を読み取る場所。API がマッピングモードの場合はこのパラメーターはオプションですが、パススルーモードの場合は必須です。有効な値:「query」および「header」。
claimParameters: # クレームパラメーターのマッピング。API Gateway は JWT クレームをバックエンドパラメーターにマッピングします。
- claimName: aud # クレーム名。パブリッククレームとプライベートクレームがサポートされています。
parameterName: X-Aud # マッピングされたパラメーターの名前。
location: header # マッピングされたパラメーターの場所。有効な値:「query」、「header」、「path」、「formData」。
- claimName: userId # クレーム名。パブリッククレームとプライベートクレームがサポートされています。
parameterName: userId # マッピングされたパラメーターの名前。
location: query # マッピングされたパラメーターの場所。有効な値:「query」、「header」、「path」、「formData」。
preventJtiReplay: false # `jti` クレームに対してアンチリプレイチェックを有効にするかどうかを指定します。デフォルト値: false。
# セクション 2.1 で生成された JSON Web キー (JWK) の公開鍵。
jwk:
kty: RSA
e: AQAB
use: sig
kid: uniq_key
alg: RS256
n: qSVxcknOm0uCq5vGsOmaorPDzHUubBmZZ4UXj-9do7w9X1uKFXAnqfto4TepSNuYU2bA_-tzSLAGBsR-BqvT6w9SjxakeiyQpVmexxnDw5WZwpWenUAcYrfSPEoNU-0hAQwFYgqZwJQMN8ptxkd0170PFauwACOx4Hfr-9FPGy8NCoIO4MfLXzJ3mJ7xqgIZp3NIOGXz-GIAbCf13ii7kSStpYqN3L_zzpvXUAos1FJ9IPXRV84tIZpFVh2lmRh0h8ImK-vI42dwlD_hOIzayL1Xno2R0T-d5AwTSdnep7g-Fwu8-sj4cCRWq3bd61Zs2QOJ8iustH0vSRMYdP5oYQ
2.4 JWT プラグインの API へのバインド
[プラグイン] ページで、作成した JWT 認証プラグインを見つけて [API のバインド] をクリックします。ダイアログボックスで、指定した API グループと環境から API を選択し、右側のリストに追加してから [OK] をクリックします。
API が同じタイプのプラグインにすでにバインドされている場合、新しいプラグインは既存のプラグインを上書きします。ご注意ください。
API Gateway コンソールの API デバッグ機能は、JWT 認証プラグインをサポートしていません。プラグインにバインドされた API をテストするには、Postman などのツールを使用するか、curl コマンドを実行してください。
3. トークン発行サービスのサンプルコード
import java.security.PrivateKey;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.lang.JoseException;
public class GenerateJwtDemo {
public static void main(String[] args) throws JoseException {
// API Gateway で設定した keyId を使用します。
String keyId = "uniq_key";
// セクション 2.1 で生成したキーペアを使用します。
String privateKeyJson = "{\n"
+ " \"kty\": \"RSA\",\n"
+ " \"d\": "
+
"\"O9MJSOgcjjiVMNJ4jmBAh0mRHF_TlaVva70Imghtlgwxl8BLfcf1S8ueN1PD7xV6Cnq8YenSKsfiNOhC6yZ_fjW1syn5raWfj68eR7cjHWjLOvKjwVY33GBPNOvspNhVAFzeqfWneRTBbga53Agb6jjN0SUcZdJgnelzz5JNdOGaLzhacjH6YPJKpbuzCQYPkWtoZHDqWTzCSb4mJ3n0NRTsWy7Pm8LwG_Fd3pACl7JIY38IanPQDLoighFfo-Lriv5z3IdlhwbPnx0tk9sBwQBTRdZ8JkqqYkxUiB06phwr7mAnKEpQJ6HvhZBQ1cCnYZ_nIlrX9-I7qomrlE1UoQ\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"kid\": \"uniq_key\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"n\": \"vCuB8MgwPZfziMSytEbBoOEwxsG7XI3MaVMoocziP4SjzU4IuWuE_DodbOHQwb_thUru57_Efe"
+
"--sfATHEa0Odv5ny3QbByqsvjyeHk6ZE4mSAV9BsHYa6GWAgEZtnDceeeDc0y76utXK2XHhC1Pysi2KG8KAzqDa099Yh7s31AyoueoMnrYTmWfEyDsQL_OAIiwgXakkS5U8QyXmWicCwXntDzkIMh8MjfPskesyli0XQD1AmCXVV3h2Opm1Amx0ggSOOiINUR5YRD6mKo49_cN-nrJWjtwSouqDdxHYP-4c7epuTcdS6kQHiQERBd1ejdpAxV4c0t0FHF7MOy9kw\"\n"
+ "}";
JwtClaims claims = new JwtClaims();
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
// 有効期限は7日未満で設定する必要があります。
NumericDate date = NumericDate.now();
date.addSeconds(120*60);
claims.setExpirationTime(date);
claims.setNotBeforeMinutesInThePast(1);
claims.setSubject("YOUR_SUBJECT");
claims.setAudience("YOUR_AUDIENCE");
// カスタムパラメーターを追加します。すべての値は String 型にする必要があります。
claims.setClaim("userId", "1213234");
claims.setClaim("email", "userEm***@youapp.com");
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
// このパラメーターは必須です。
jws.setKeyIdHeaderValue(keyId);
jws.setPayload(claims.toJson());
PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyJson)).getPrivateKey();
jws.setKey(privateKey);
String jwtResult = jws.getCompactSerialization();
System.out.println("JSON Web トークンを生成しました。結果は " + jwtResult);
}
}
次の点にご注意ください。
-
keyIdはグローバルに一意である必要があり、次の 3 つの場所で一致させる必要があります。
-
「2.1 JSON Web キー (JWK) ペアの生成」セクションでキーペアを生成するときに指定する
keyId。 -
「2.3 JWT プラグインの設定」セクションで JWT 認証プラグインに設定する
kid。 -
コード内の
keyId。これはJsonWebSignatureオブジェクトのKeyIdHeaderValueプロパティの値です。このプロパティは必須です。
-
privateKeyJsonには、秘密鍵の JWK JSON 文字列を使用します。これは、「セクション 2.1」でオンラインで生成したもの、またはキーペアをローカルで生成した場合はprivateKeyStringJSON 文字列を使用できます。privateKeyStringは JSON 文字列です。 -
有効期間は必須で、7日未満に設定する必要があります。
-
すべてのカスタムパラメーターの値は String 型にしてください。
4. API Gateway のエラーレスポンス
|
ステータス |
コード |
メッセージ |
説明 |
|
400 |
I400JR |
JWT required |
JWT パラメーターが見つかりません。 |
|
403 |
S403JI |
Claim jti is required when preventJtiReplay:true |
アンチリプレイチェックが有効ですが、 |
|
403 |
S403JU |
Claim jti in JWT is used |
提供された |
|
403 |
A403JT |
Invalid JWT: ${Reason} |
リクエストで提供された JWT は無効です。 |
|
400 |
I400JD |
JWT Deserialize Failed: ${Token} |
リクエストで提供された JWT のデシリアライズに失敗しました。 |
|
403 |
A403JK |
No matching JWK, kid:${kid} not found |
JWT の |
|
403 |
A403JE |
JWT is expired at ${Date} |
リクエストで提供された JWT は期限切れです。 |
|
400 |
I400JP |
Invalid JWT plugin config: ${JWT} |
JWT 認証プラグインが正しく設定されていません。 |
予期しないステータスコードを受け取った場合は、X-Ca-Error-Code レスポンスヘッダーで ErrorCode を、X-Ca-Error-Message ヘッダーで ErrorMessage を確認してください。エラーコードが A403JT または I400JD の場合は、jwt.io を使用して token の有効性とフォーマットを検証してください。