Spring WebClientおよびOAuth2のサポート

1. 概要

Spring Security 5は、Spring Webfluxのノンブロッキング_https://www.baeldung.com/spring-5-webclient [WebClient] _クラスのOAuth2サポートを提供します。
このチュートリアルでは、このクラスを使用して保護されたリソースにアクセスするさまざまなアプローチを分析します。
また、SpringがOAuth2承認プロセスをどのように処理するかを理解するために、内部を見ていきます。

2. シナリオのセットアップ

  • https://tools.ietf.org/html/rfc6749 [OAuth2仕様]に沿って、この記事の主な対象であるクライアントとは別に、当然、承認サーバーとリソースサーバーが必要です。 *

    GoogleやGithubなどの有名な認証プロバイダーを使用できます。 OAuth2クライアントの役割をよりよく理解するために、独自のサーバー、https://github.com/Baeldung/spring-security-oauth [ここで利用可能な実装]を使用することもできます。 このチュートリアルのトピックではないため、完全な構成は表示しません。次のことを十分に理解しています。
  • 認可サーバーは次のようになります。

  • ポート_8081_で実行

  • _ / oauth / authorize、_ _ / oauth / token_の公開
    および_oauth / check_token_エンドポイントは、目的の機能を実行します

  • サンプルユーザーで構成(例: john _ / _ 123)および単一のOAuth
    クライアント(fooClientIdPassword _ / _ secret

  • リソースサーバーは認証サーバーから分離されます
    そして:

  • ポート_8082_で実行

  • を使用してアクセスできる単純な_Foo_オブジェクトのセキュリティで保護されたリソースを提供する
    / foos / \ {id} endpoint

    *注意:複数のSpringプロジェクトが異なるOAuth関連の機能と実装を提供していることを理解することが重要です。 https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix[this Spring Projects matrix]で各ライブラリが提供するものを調べることができます。*
    _WebClient_およびすべてのリアクティブWebflux関連機能は、Spring Security 5プロジェクトの一部です。 したがって、この記事では主にこのフレームワークを使用します。

3. 内部のSpring Security 5

今後の例を完全に理解するには、Spring SecurityがOAuth2機能を内部で管理する方法を知っておくとよいでしょう。
このフレームワークには次の機能があります。
  • OAuth2プロバイダーアカウントに依存して
    link:/spring-security-5-oauth2-login [ログインアプリケーションへのユーザー]

  • サービスをOAuth2クライアントとして構成する

  • 承認手続きを管理する

  • トークンを自動的に更新する

  • 必要に応じて資格情報を保存する

    次の図に、Spring SecurityのOAuth2ワールドの基本概念の一部を示します。
    link:/uploads/websecurity_webclient_oauth2-100x55.png%20100w []

3.1. プロバイダ

Springは、OAuth 2.0で保護されたリソースを公開するOAuth2プロバイダーロールを定義します。
この例では、認証サービスがプロバイダー機能を提供するサービスになります。

3.2. クライアント登録

A _ClientRegistration_は、OAuth2(またはOpenID)プロバイダーに登録された特定のクライアントのすべての関連情報を含むエンティティです。
このシナリオでは、_bael-client-id_ idで識別される認証サーバーに登録されたクライアントになります。

3.3. 認定クライアント

エンドユーザー(リソース所有者)がクライアントにリソースへのアクセス許可を付与すると、__OAuth2AuthorizedClient __entityが作成されます。
アクセストークンをクライアント登録とリソース所有者(_Principal_オブジェクトで表される)に関連付ける責任があります。

3.4. リポジトリ

さらに、Spring Securityは、上記のエンティティにアクセスするためのリポジトリクラスも提供します。
特に、__ReactiveClientRegistrationRepository __および_ServerOAuth2AuthorizedClientRepository_クラスはリアクティブスタックで使用され、デフォルトではインメモリストレージを使用します。
  • Spring Boot 2.xは、これらのリポジトリクラスのBeanを作成し、コンテキストに自動的に追加します。*

3.5. セキュリティWebフィルターチェーン

Spring Security 5の重要な概念の1つは、リアクティブな__SecurityWebFilterChain __entityです。
その名前が示すように、https://www.baeldung.com/spring-webflux-filters [_WebFilter_]オブジェクトの連鎖コレクションを表します。
アプリケーションでOAuth2機能を有効にすると、Spring Securityはチェーンに2つのフィルターを追加します。
  1. 1つのフィルターが許可要求に応答します
    (_ / oauth2 / authorization / \ {registrationId} _ URI)または_ClientAuthorizationRequiredException_をスローします。 これには、ReactiveClientRegistrationRepositoryへの参照、が含まれており、ユーザーエージェントをリダイレクトするための承認リクエストの作成を担当しています。

  2. 2番目のフィルターは、追加する機能によって異なります
    (OAuth2クライアント機能またはOAuth2ログイン機能)。 どちらの場合も、このフィルターの主な役割は、OAuth2AuthorizedClient instanceを作成し、_ServerOAuth2AuthorizedClientRepository._を使用して保存することです。

3.6. Webクライアント

Webクライアントは、リポジトリへの参照を含む_ExchangeFilterFunction_で構成されます。
それらを使用してアクセストークンを取得し、リクエストに自動的に追加します。

4. Spring Security 5のサポート–クライアント資格情報フロー

Spring Securityでは、アプリケーションをOAuth2クライアントとして構成できます。
*この記事では、_WebClient_インスタンスを使用して、最初に「Client Credentials」__ ___grantタイプを使用し、次に「Authorization Code」フローを使用してリソースを取得します。*
最初に行う必要があるのは、アクセストークンを取得するために使用するクライアント登録とプロバイダーを構成することです。

4.1. クライアントとプロバイダーの構成

link:/spring-security-5-oauth2-login#setup[OAuth2ログインの記事]で見たように、プログラムで設定するか、Spring Bootの自動設定に依存することができます。プロパティを使用して登録を定義する:
spring.security.oauth2.client.registration.bael.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret

spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token
これらはすべて、__client_credentials ___flowを使用してリソースを取得するために必要な構成です。

4.2. _WebClient_を使用する

*この付与タイプは、アプリケーションと対話するエンドユーザーがいないマシン間通信で使用します。*
たとえば、アプリケーションで_WebClient_を使用してセキュリティで保護されたリソースを取得しようとする_cron_ジョブがあるとします。
@Autowired
private WebClient webClient;

@Scheduled(fixedRate = 5000)
public void logResourceServiceResponse() {

    webClient.get()
      .uri("http://localhost:8084/retrieve-resource")
      .retrieve()
      .bodyToMono(String.class)
      .map(string
        -> "Retrieved using Client Credentials Grant Type: " + string)
      .subscribe(logger::info);
}

4.3. _WebClient_の構成

次に、スケジュールされたタスクで自動配線した_webClient_ instanceを設定しましょう。
@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    oauth.setDefaultClientRegistrationId("bael");
    return WebClient.builder()
      .filter(oauth)
      .build();
}
前述したように、クライアント登録リポジトリはSpring Bootによって自動的に作成され、コンテキストに追加されます。
ここで次に注意することは、__UnAuthenticatedServerOAuth2AuthorizedClientRepository __instanceを使用していることです。 これは、マシン間の通信であるため、エンドユーザーがプロセスに参加しないという事実によるものです。 最後に、デフォルトで__bael ___client登録を使用すると述べました。
それ以外の場合は、cronジョブでリクエストを定義するまでに指定する必要があります。
webClient.get()
  .uri("http://localhost:8084/retrieve-resource")
  .attributes(
    ServerOAuth2AuthorizedClientExchangeFilterFunction
      .clientRegistrationId("bael"))
  .retrieve()
  // ...

4.4. テスト

_DEBUG_ログレベルを有効にしてアプリケーションを実行すると、Spring Securityが行っている呼び出しを確認できます。
o.s.w.r.f.client.ExchangeFunctions:
  HTTP POST http://localhost:8085/oauth/token
o.s.http.codec.json.Jackson2JsonDecoder:
  Decoded [{access_token=89cf72cd-183e-48a8-9d08-661584db4310,
    token_type=bearer,
    expires_in=41196,
    scope=read
    (truncated)...]
o.s.w.r.f.client.ExchangeFunctions:
  HTTP GET http://localhost:8084/retrieve-resource
o.s.core.codec.StringDecoder:
  Decoded "This is the resource!"
c.b.w.c.service.WebClientChonJob:
  We retrieved the following resource using Client Credentials Grant Type: This is the resource!
また、タスクが2回実行されると、最後のトークンの有効期限が切れていないため、アプリケーションは最初にトークンを要求せずにリソースを要求します。

5. Spring Security 5のサポート–承認コードフローを使用した実装

*この許可タイプは通常、信頼性の低いサードパーティアプリケーションがリソースにアクセスする必要がある場合に使用されます。*

5.1. クライアントとプロバイダーの構成

承認コードフローを使用してOAuth2プロセスを実行するには、クライアント登録とプロバイダーのプロパティをさらに定義する必要があります。
spring.security.oauth2.client.registration.bael.client-name=bael
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret
spring.security.oauth2.client.registration.bael
  .authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.bael
  .redirect-uri=http://localhost:8080/login/oauth2/code/bael

spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token
spring.security.oauth2.client.provider.bael
  .authorization-uri=http://localhost:8085/oauth/authorize
spring.security.oauth2.client.provider.bael.user-info-uri=http://localhost:8084/user
spring.security.oauth2.client.provider.bael.user-name-attribute=name
プロパティとは別に、前のセクションで使用しましたが、今回は以下も含める必要があります。
  • 認証サーバーで認証するエンドポイント

  • ユーザー情報を含むエンドポイントのURL

  • ユーザーエージェントがアクセスするアプリケーションのエンドポイントのURL
    認証後にリダイレクトされる

    もちろん、有名なプロバイダーの場合、最初の2つのポイントを指定する必要はありません。
    リダイレクトエンドポイントは、Spring Securityによって自動的に作成されます。
    デフォルトでは、設定されたURLは_ / [action] / oauth2 / code / [registrationId]、_であり、__authorize ___and _login_アクションのみが許可されています(無限ループを回避するため)。
    このエンドポイントは以下を担当します。
  • クエリパラメータとして認証コードを受け取る

  • これを使用してアクセストークンを取得する

  • 承認済みクライアントインスタンスの作成

  • ユーザーエージェントを元のエンドポイントにリダイレクトする

5.2. HTTPセキュリティ構成

次に、_SecurityWebFilterChain._を構成する必要があります。
最も一般的なシナリオは、Spring SecurityのOAuth2ログイン機能を使用してユーザーを認証し、エンドポイントとリソースへのアクセスを許可することです。
その場合は、*アプリケーションをOAuth2クライアントとして機能させるには、__ServerHttpSecurity __definitionに_oauth2Login_ディレクティブを含めるだけで十分です。*
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange()
      .anyExchange()
      .authenticated()
      .and()
      .oauth2Login();
    return http.build();
}

5.3. _WebClient_の構成

ここで、_WebClient_インスタンスを配置します。
@Bean
WebClient webClient(
  ReactiveClientRegistrationRepository clientRegistrations,
  ServerOAuth2AuthorizedClientRepository authorizedClients) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        authorizedClients);
    oauth.setDefaultOAuth2AuthorizedClient(true);
    return WebClient.builder()
      .filter(oauth)
      .build();
}
今回は、コンテキストからクライアント登録リポジトリと承認済みクライアントリポジトリの両方を注入しています。
また、__setDefaultOAuth2AuthorizedClient ___optionを有効にします。 これにより、フレームワークはSpring Securityで管理されている現在の_Authentication_オブジェクトからクライアント情報を取得しようとします。
それを考慮すると、すべてのHTTP要求にアクセストークンが含まれることになりますが、これは望ましい動作ではない可能性があります。
後で代替を分析して、特定の_WebClient_トランザクションが使用するクライアントを示します。

5.4. _WebClient_を使用する

*認証コードには、プロシージャを実行するためのリダイレクト(ブラウザなど)を解決できるユーザーエージェントが必要です。*
したがって、ユーザーがアプリケーションと対話するときに、通常はHTTPエンドポイントを呼び出して、この付与タイプを使用します。
@RestController
public class ClientRestController {

    @Autowired
    WebClient webClient;

    @GetMapping("/auth-code")
    Mono<String> useOauthWithAuthCode() {
        Mono<String> retrievedResource = webClient.get()
          .uri("http://localhost:8084/retrieve-resource")
          .retrieve()
          .bodyToMono(String.class);
        return retrievedResource.map(string ->
          "We retrieved the following resource using Oauth: " + string);
    }
}

5.5. テスト

最後に、エンドポイントを呼び出し、ログエントリをチェックして、何が起こっているかを分析します。
エンドポイントを呼び出した後、アプリケーションは、アプリケーションでまだ認証されていないことを確認します。
o.s.w.s.adapter.HttpWebHandlerAdapter: HTTP GET "/auth-code"
...
HTTP/1.1 302 Found
Location: /oauth2/authorization/bael
アプリケーションは、承認サービスのエンドポイントにリダイレクトして、プロバイダーのレジストリに存在する資格情報を使用して認証します(この場合、_bael-user / bael-password_を使用します)。
HTTP/1.1 302 Found
Location: http://localhost:8085/oauth/authorize
  ?response_type=code
  &client_id=bael-client-id
  &state=...
  &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Foauth2%2Fcode%2Fbael
認証後、ユーザーエージェントは、クエリパラメーターとしてのコードと最初に送信された状態値と共にリダイレクトURIに返送されます(回避するために、https://spring.io/blog/2011/11/30/クロスサイトリクエストフォージェリおよびoauth2 [CSRF攻撃]):
o.s.w.s.adapter.HttpWebHandlerAdapter:HTTP GET "/login/oauth2/code/bael?code=...&state=...
次に、アプリケーションはコードを使用してアクセストークンを取得します。
o.s.w.r.f.client.ExchangeFunctions:HTTP POST http://localhost:8085/oauth/token
ユーザー情報を取得します。
o.s.w.r.f.client.ExchangeFunctions:HTTP GET http://localhost:8084/user
そして、ユーザーエージェントを元のエンドポイントにリダイレクトします。
HTTP/1.1 302 Found
Location: /auth-code
最後に、_WebClient_インスタンスは、セキュリティで保護されたリソースを正常に要求できます。
o.s.w.r.f.client.ExchangeFunctions:HTTP GET http://localhost:8084/retrieve-resource
o.s.w.r.f.client.ExchangeFunctions:Response 200 OK
o.s.core.codec.StringDecoder :Decoded "This is the resource!"

6. 代替–通話中のクライアント登録

前に、__setDefaultOAuth2AuthorizedClient ___を使用すると、アプリケーションがクライアントで実現する呼び出しにアクセストークンを含めることを意味することがわかりました。
このコマンドを構成から削除する場合、リクエストを定義するまでにクライアント登録を明示的に指定する必要があります。
もちろん、クライアント資格情報フローで作業するときに以前に行ったように、did__clientRegistrationId ___を使用する方法もあります。
  • _Principal_を承認済みクライアントに関連付けたため、 @ RegisteredOAuth2AuthorizedClient annotationを使用してOAuth2AuthorizedClient instanceを取得できます。*

@GetMapping("/auth-code-annotated")
Mono<String> useOauthWithAuthCodeAndAnnotation(
  @RegisteredOAuth2AuthorizedClient("bael") OAuth2AuthorizedClient authorizedClient) {
    Mono<String> retrievedResource = webClient.get()
      .uri("http://localhost:8084/retrieve-resource")
      .attributes(
        ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient))
      .retrieve()
      .bodyToMono(String.class);
    return retrievedResource.map(string ->
      "Resource: " + string
        + " - Principal associated: " + authorizedClient.getPrincipalName()
        + " - Token will expire at: " + authorizedClient.getAccessToken()
          .getExpiresAt());
}

7. OAuth2ログイン機能の回避

前述したように、最も一般的なシナリオは、OAuth2承認プロバイダーに依存してアプリケーションにユーザーをログインさせることです。
しかし、これを回避したいが、OAuth2プロトコルを使用して保護されたリソースにアクセスできる場合はどうでしょうか? 次に、構成を変更する必要があります。
まず最初に、そしてわかりやすくするために、リダイレクトURIプロパティを定義するときに__login __oneの代わりに__authorize __actionを使用できます。
spring.security.oauth2.client.registration.bael
  .redirect-uri=http://localhost:8080/login/oauth2/code/bael
アプリケーションに_Principal_を作成するために使用しないため、ユーザー関連のプロパティを削除することもできます。
*ここで、_oauth2Login_コマンドを含めずに__SecurityWebFilterChain __を構成し、代わりに_oauth2Client_を含めます。*
OAuth2ログインに依存する必要はありませんが、エンドポイントにアクセスする前にユーザーを認証する必要があります。 このため、ここに_formLogin_ディレクティブも含めます。
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange()
      .anyExchange()
      .authenticated()
      .and()
      .oauth2Client()
      .and()
      .formLogin();
    return http.build();
}
アプリケーションを実行して、__ / auth-code-annotated ___endpointを使用したときに何が起こるかを確認しましょう。
まず、フォームloginを使用してアプリケーションにログインする必要があります。
その後、アプリケーションは、リソースへのアクセスを許可するために、承認サービスログインにリダイレクトします。
*注意:これを実行した後、呼び出した元のエンドポイントにリダイレクトする必要があります。 それでも、Spring Securityはルートパス「/」にリダイレクトしているようです。これはバグのようです。 OAuth2ダンスをトリガーした後の次のリクエストは正常に実行されます。*
エンドポイントの応答では、認証されたクライアントが、認証サービスで構成されたユーザーの後の__bael-user、__nameの代わりに、__ bael-client-id ___という名前のプリンシパルに関連付けられていることがわかります。

8. Spring Frameworkサポート–手動アプローチ

すぐに使用できる* Spring 5は、ベアラートークンヘッダーをリクエストに簡単に追加するためのOAuth2関連のサービスメソッドを1つだけ提供します。 それは__HttpHeaders#setBearerAuth __method。*です
OAuth2ダンスを手動で実行して、セキュリティで保護されたリソースを取得するために必要なことを理解するための例を見てみましょう。
簡単に言えば、2つのHTTP要求をチェーンする必要があります。1つは承認サーバーから認証トークンを取得し、もう1つはこのトークンを使用してリソースを取得します。
@Autowired
WebClient client;

public Mono<String> obtainSecuredResource() {
    String encodedClientData =
      Base64Utils.encodeToString("bael-client-id:bael-secret".getBytes());
    Mono<String> resource = client.post()
      .uri("localhost:8085/oauth/token")
      .header("Authorization", "Basic " + encodedClientData)
      .body(BodyInserters.fromFormData("grant_type", "client_credentials"))
      .retrieve()
      .bodyToMono(JsonNode.class)
      .flatMap(tokenResponse -> {
          String accessTokenValue = tokenResponse.get("access_token")
            .textValue();
          return client.get()
            .uri("localhost:8084/retrieve-resource")
            .headers(h -> h.setBearerAuth(accessTokenValue))
            .retrieve()
            .bodyToMono(String.class);
        });
    return resource.map(res ->
      "Retrieved the resource using a manual approach: " + res);
}
この例の主な目的は、OAuth2仕様に準拠したリクエストを活用することの面倒さを理解し、_setBearerAuth_メソッドの使用方法を確認することです。
*実際のシナリオでは、前のセクションで行ったように、Spring Securityがすべてのハードワークを透過的に処理できるようにします。*

9. 結論

このチュートリアルでは、アプリケーションをOAuth2クライアントとして設定する方法、特に_WebClient_を構成して使用して、完全に反応するスタックでセキュリティで保護されたリソースを取得する方法を説明しました。
最後になりましたが、Spring Security 5 OAuth2メカニズムがOAuth2仕様に準拠するために内部でどのように動作するかを分析しました。
いつものように、完全な例は、https://github.com/eugenp/tutorials/tree/master/spring-5-reactive-oauth [Github]で入手できます。