1. 序章
Spring Cloud Gatewayは、Spring Bootに基づいて軽量のAPIゲートウェイをすばやく作成できるライブラリです。これについては、以前の記事ですでに説明しました。
今回は、その上にOAuth2.0パターンをすばやく実装する方法を紹介します。
2. OAuth2.0クイック要約
OAuth 2.0標準は、ユーザーとアプリケーションがリソースに安全にアクセスできるセキュリティメカニズムとして、インターネット全体で使用されている確立された標準です。
この標準を詳細に説明することはこの記事の範囲を超えていますが、いくつかの重要な用語の簡単な要約から始めましょう。
- リソース:許可されたクライアントのみが取得できるあらゆる種類の情報
- クライアント:通常はRESTAPIを介してリソースを消費するアプリケーション
- リソースサーバー:承認されたクライアントへのリソースの提供を担当するサービス
- リソース所有者:リソースを所有し、最終的にはクライアントにリソースへのアクセスを許可する責任があるエンティティ(人間またはアプリケーション)
- トークン:クライアントによって取得され、認証要求の一部としてリソースサーバーに送信される情報の一部
- IDプロバイダー(IdP):ユーザーの資格情報を検証し、クライアントにアクセストークンを発行します。
- 認証フロー:有効なトークンを取得するためにクライアントが実行する必要のある一連の手順。
標準の包括的な説明については、このトピックに関するAuth0のドキュメントから始めることをお勧めします。
3. OAuth2.0パターン
Spring Cloud Gatewayは、主に次のいずれかの役割で使用されます。
- OAuthクライアント
- OAuthリソースサーバー
これらの各ケースについて詳しく説明しましょう。
3.1. OAuth2.0クライアントとしてのSpringCloudGateway
このシナリオでは、認証されていない着信要求が認証コードフローを開始します。 トークンがゲートウェイによって取得されると、バックエンドサービスにリクエストを送信するときに使用されます。
このパターンの実際の良い例は、ソーシャルネットワークフィードアグリゲーターアプリケーションです。サポートされているネットワークごとに、ゲートウェイはOAuth2.0クライアントとして機能します。
その結果、フロントエンド(通常、Angular、React、または同様のUIフレームワークで構築されたSPAアプリケーション)は、エンドユーザーに代わってこれらのネットワーク上のデータにシームレスにアクセスできます。 さらに重要なこと:ユーザーがアグリゲーターに自分の資格情報を公開することなくこれを行うことができます。
3.2. OAuth2.0リソースサーバーとしてのSpringCloudGateway
ここで、ゲートウェイはゲートキーパーとして機能し、バックエンドサービスに送信する前にすべてのリクエストに有効なアクセストークンがあることを強制します。 さらに、関連付けられたスコープに基づいて、トークンが特定のリソースにアクセスするための適切な権限を持っているかどうかを確認することもできます。
この種の許可チェックは主に大まかなレベルで機能することに注意することが重要です。 きめ細かいアクセス制御(オブジェクト/フィールドレベルのアクセス許可など)は、通常、ドメインロジックを使用してバックエンドで実装されます。 このパターンで考慮すべきことの1つは、バックエンドサービスが転送された要求を認証および承認する方法です。 主なケースは2つあります。
- トークンの伝播:APIゲートウェイは、受信したトークンをそのままバックエンドに転送します
- トークンの置換:API Gatewayは、リクエストを送信する前に、着信トークンを別のトークンに置き換えます。
このチュートリアルでは、最も一般的なシナリオであるトークン伝播のケースのみを取り上げます。 2つ目も可能ですが、ここで示したい主要なポイントから注意をそらすような追加のセットアップとコーディングが必要です。
4. サンプルプロジェクトの概要
これまでに説明したOAuthパターンでSpringGatewayを使用する方法を示すために、単一のエンドポイント / quotes /{symbol}を公開するサンプルプロジェクトを作成してみましょう。 このエンドポイントへのアクセスには、構成されたIDプロバイダーによって発行された有効なアクセストークンが必要です。
この例では、埋め込みKeycloakIDプロバイダーを使用します。 必要な変更は、新しいクライアントアプリケーションとテスト用の数人のユーザーの追加だけです。
もう少し面白くするために、バックエンドサービスは、リクエストに関連付けられたユーザーに応じて異なる見積もり価格を返します。 ゴールドの役割を持つユーザーはより低い価格を取得しますが、他のすべてのユーザーは通常の価格を取得します(結局のところ、人生は不公平です; ^))。
このサービスの前にSpringCloudGatewayを配置し、構成を数行変更するだけで、その役割をOAuthクライアントからリソースサーバーに切り替えることができます。
5. プロジェクトの設定
5.1. Keycloak IdP
このチュートリアルで使用するEmbeddedKeycloakは、 GitHub からクローンを作成し、Mavenでビルドできる通常のSpringBootアプリケーションです。
$ git clone https://github.com/Baeldung/spring-security-oauth
$ cd oauth-rest/oauth-authorization/server
$ mvn install
注:このプロジェクトは現在Java 13以降を対象としていますが、Java11でも正常にビルドおよび実行されます。 Mavenのコマンドに-Djava.version=11を追加するだけです。
次に、 src / main / resources /baeldung-domain.jsonをthisoneに置き換えます。 変更されたバージョンは、元のバージョンと同じ構成に加えて、追加のクライアントアプリケーション( quotes-client )、2つのユーザーグループ(golden_およびsilver_customers)、および2つの役割(ゴールドおよびシルバー)。
これで、 spring-boot:runmavenプラグインを使用してサーバーを起動できます。
$ mvn spring-boot:run
... many, many log messages omitted
2022-01-16 10:23:20.318
INFO 8108 --- [ main] c.baeldung.auth.AuthorizationServerApp : Started AuthorizationServerApp in 23.815 seconds (JVM running for 24.488)
2022-01-16 10:23:20.334
INFO 8108 --- [ main] c.baeldung.auth.AuthorizationServerApp : Embedded Keycloak started: http://localhost:8083/auth to use keycloak
サーバーが起動したら、ブラウザで http:// localhost:8083 / auth / admin / master / console /#/ realms /baeldungにアクセスしてアクセスできます。 管理者の資格情報( bael-admin / pass )でログインすると、レルムの管理画面が表示されます。
IdPのセットアップを完了するために、2人のユーザーを追加しましょう。 最初のものは、golden_customerグループのメンバーであるMaxwellSmartになります。 2つ目はJohnSnowで、これはどのグループにも追加しません。
提供された構成を使用すると、golden_customersグループのメンバーは自動的にgoldの役割を引き受けます。
5.2. バックエンドサービス
quotesバックエンドには、通常のSpring Boot Reactive MVC依存関係に加えて、リソースサーバースターター依存関係が必要です。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.2</version>
</dependency>
依存関係のバージョンを意図的に省略していることに注意してください。 これは、SpringBootの親POMまたは依存関係管理セクションで対応するBOMを使用する場合に推奨される方法です。
メインアプリケーションクラスでは、@EnableWebFluxSecurityを使用してWebフラックスセキュリティを有効にする必要があります。
@SpringBootApplication
@EnableWebFluxSecurity
public class QuotesApplication {
public static void main(String[] args) {
SpringApplication.run(QuotesApplication.class);
}
}
エンドポイントの実装では、提供されている BearerAuthenticationToken を使用して、現在のユーザーがgoldの役割を持っているかどうかを確認します。
@RestController
public class QuoteApi {
private static final GrantedAuthority GOLD_CUSTOMER = new SimpleGrantedAuthority("gold");
@GetMapping("/quotes/{symbol}")
public Mono<Quote> getQuote(@PathVariable("symbol") String symbol,
BearerTokenAuthentication auth ) {
Quote q = new Quote();
q.setSymbol(symbol);
if ( auth.getAuthorities().contains(GOLD_CUSTOMER)) {
q.setPrice(10.0);
}
else {
q.setPrice(12.0);
}
return Mono.just(q);
}
}
では、Springはどのようにしてユーザーロールを取得しますか? 結局のところ、これはscopesやemailのような標準的な主張ではありません。確かに、ここに魔法はありません。返されたカスタムフィールドからこれらの役割を抽出するカスタムReactiveOpaqueTokenIntrospectionを提供する必要がありますKeycloakによる。 オンラインで入手できるこのBeanは、基本的に、このトピックに関するSpringのドキュメントに示されているものと同じですが、カスタムフィールドに固有の小さな変更がいくつかあります。
また、IDプロバイダーにアクセスするために必要な構成プロパティを提供する必要があります。
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=quotes-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<CLIENT SECRET>
最後に、アプリケーションを実行するには、IDEにインポートするか、Mavenから実行します。 プロジェクトのPOMには、この目的のためのプロファイルが含まれています。
$ mvn spring-boot:run -Pquotes-application
これで、アプリケーションは http:// localhost:8085 /quotesでリクエストを処理できるようになります。 curl を使用して、応答していることを確認できます。
$ curl -v http://localhost:8085/quotes/BAEL
予想どおり、 Authorization ヘッダーが送信されなかったため、 401Unauthorized応答が返されます。
6. OAuth2.0リソースサーバーとしてのSpringGateway
リソースサーバーとして機能するSpringCloudGatewayアプリケーションの保護は、通常のリソースサービスと同じです。したがって、バックエンドサービスの場合と同じスターター依存関係を追加する必要があるのは当然です。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.2</version>
</dependency>
したがって、@EnableWebFluxSecurityもスタートアップクラスに追加する必要があります。
@SpringBootApplication
@EnableWebFluxSecurity
public class ResourceServerGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerGatewayApplication.class,args);
}
}
セキュリティ関連の構成プロパティは、バックエンドで使用されているものと同じです。
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
client-id: quotes-client
client-secret: <code class="language-css"><CLIENT SECRET>
次に、SpringCloudGatewayのセットアップに関する以前の記事で行ったのと同じ方法でルート宣言を追加します。
... other properties omitted
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**
セキュリティの依存関係とプロパティを除いて、ゲートウェイ自体では何も変更されていないことに注意してください。 ゲートウェイアプリケーションを実行するには、 spring -boot:run を使用し、必要な設定で特定のプロファイルを使用します。
$ mvn spring-boot:run -Pgateway-as-resource-server
6.1. リソースサーバーのテスト
パズルのピースがすべて揃ったので、それらをまとめましょう。 まず、Keycloak、quotesバックエンド、ゲートウェイがすべて実行されていることを確認する必要があります。
次に、Keycloakからアクセストークンを取得する必要があります。この場合、アクセストークンを取得する最も簡単な方法は、パスワード付与フロー(別名「リソース所有者」)を使用することです。 これは、QuotesクライアントアプリケーションのクライアントIDとシークレットとともに、ユーザーの1人のユーザー名/パスワードを渡すKeycloakへのPOSTリクエストを実行することを意味します。
$ curl -L -X POST \
'http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=quotes-client' \
--data-urlencode 'client_secret=0e082231-a70d-48e8-b8a5-fbfb743041b6' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=email roles profile' \
--data-urlencode 'username=john.snow' \
--data-urlencode 'password=1234'
応答は、アクセストークンと他の値を含むJSONオブジェクトになります。
{
"access_token": "...omitted",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "...omitted",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "7fd04839-fab1-46a7-a179-a2705dab8c6b",
"scope": "profile email"
}
これで、返されたアクセストークンを使用して、 / quotesAPIにアクセスできます。
$ curl --location --request GET 'http://localhost:8086/quotes/BAEL' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer xxxx...'
これはJSON形式で見積もりを生成します:
{
"symbol":"BAEL",
"price":12.0
}
今回はMaxwellSmartのアクセストークンを使用して、このプロセスを繰り返しましょう。
{
"symbol":"BAEL",
"price":10.0
}
価格が安いことがわかります。これは、バックエンドが関連付けられたユーザーを正しく識別できたことを意味します。 Authorization ヘッダーのないcurlリクエストを使用して、認証されていないリクエストがバックエンドに伝播されないことを確認することもできます。
$ curl http://localhost:8086/quotes/BAEL
ゲートウェイログを調べると、リクエスト転送プロセスに関連するメッセージがないことがわかります。これは、応答がゲートウェイで生成されたことを示しています。
7. OAuth2.0クライアントとしてのSpringGateway
スタートアップクラスには、リソースサーバーバージョンですでに使用しているものと同じものを使用します。 これを使用して、すべてのセキュリティ動作が利用可能なライブラリとプロパティに由来することを強調します。
実際、両方のバージョンを比較する場合の唯一の顕著な違いは、構成プロパティにあります。 ここでは、 issuer-uri プロパティ、またはさまざまなエンドポイント(承認、トークン、イントロスペクション)の個々の設定を使用して、プロバイダーの詳細を構成する必要があります。
また、要求されたスコープを含む、アプリケーションクライアント登録の詳細を定義する必要があります。 これらのスコープは、イントロスペクションメカニズムを通じて利用できる情報アイテムのセットをIdPに通知します。
... other propeties omitted
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:8083/auth/realms/baeldung
registration:
quotes-client:
provider: keycloak
client-id: quotes-client
client-secret: <CLIENT SECRET>
scope:
- email
- profile
- roles
最後に、ルート定義セクションに1つの重要な変更があります。 アクセストークンの伝播が必要なルートにTokenRelayフィルターを追加する必要があります:
spring:
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**
filters:
- TokenRelay=
または、すべてのルートで認証フローを開始する場合は、TokenRelayフィルターをdefault-filtersセクションに追加できます。
spring:
cloud:
gateway:
default-filters:
- TokenRelay=
routes:
... other routes definition omitted
7.1. SpringGatewayをOAuth2.0クライアントとしてテストする
テストのセットアップでは、プロジェクトの3つの部分が実行されていることも確認する必要があります。 ただし、今回は、OAuth2.0クライアントとして機能させるために必要なプロパティを含む別のSpringプロファイルを使用してゲートウェイを実行します。 サンプルプロジェクトのPOMには、このプロファイルを有効にしてプロジェクトを開始できるプロファイルが含まれています。
$ mvn spring-boot:run -Pgateway-as-oauth-client
ゲートウェイが実行されたら、ブラウザでhttp:// localhost:8087 / quotes/BAELを指定してゲートウェイをテストできます。 すべてが期待どおりに機能している場合は、IdPのログインページにリダイレクトされます。
Maxwell Smartのクレデンシャルを使用したので、再び低価格で見積もりを取得します。
テストを終了するために、匿名/シークレットブラウザウィンドウを使用して、JohnSnowのクレデンシャルを使用してこのエンドポイントをテストします。 今回は通常の見積もり価格を取得します。
8. 結論
この記事では、OAuth 2.0のセキュリティパターンのいくつかと、SpringCloudGatewayを使用してそれらを実装する方法について説明しました。 いつものように、すべてのコードはGitHubでを介して利用できます。