1. 概要

Spring Security 5は、SpringWebfluxのノンブロッキングWebClientクラスのOAuth2サポートを提供します。

このチュートリアルでは、このクラスを使用して保護されたリソースにアクセスするためのさまざまなアプローチを分析します。

また、SpringがOAuth2認証プロセスをどのように処理するかを理解するために内部を見ていきます。

2. シナリオの設定

OAuth2仕様に沿って、この記事で焦点を当てているクライアントは別として、当然、承認サーバーとリソースサーバーが必要です。 

GoogleやGithubなどの有名な承認プロバイダーを使用できます。 OAuth2クライアントの役割をよりよく理解するために、独自のサーバーを使用して、ここで実装を利用することもできます。 このチュートリアルのトピックではないため、完全な構成は示しません。次のことを知っていれば十分です。

  • 承認サーバーは次のようになります。
    • ポート8081で実行中
    • / oauth / authorize、 / oauth / token oauth / check_token エンドポイントを公開して、目的の機能を実行します
    • サンプルユーザーで構成(例:  john / 123 )および単一のOAuthクライアント( fooClientIdPassword / secret
  • リソースサーバーは認証サーバーから分離され、次のようになります。
    • ポート8082で実行中
    • / foos /{id}エンドポイントを使用してアクセス可能な単純なFooオブジェクトで保護されたリソースを提供する

注:いくつかのSpringプロジェクトが、さまざまなOAuth関連の機能と実装を提供していることを理解することが重要です。 このSpringProjectsマトリックスで各ライブラリが提供するものを調べることができます。

WebClient およびすべてのリアクティブWebflux関連機能は、SpringSecurity5プロジェクトの一部です。 したがって、この記事では主にこのフレームワークを使用します。

3. Spring Security 5 Under the Hood

今後の例を完全に理解するには、SpringセキュリティがOAuth2機能を内部で管理する方法を知っておくとよいでしょう。

このフレームワークは、次の機能を提供します。

  • ユーザーをアプリケーションにログインさせるには、OAuth2プロバイダーアカウントに依存します
  • サービスをOAuth2クライアントとして構成する
  • 私たちの承認手続きを管理する
  • トークンを自動的に更新
  • 必要に応じてクレデンシャルを保存します

SpringセキュリティのOAuth2ワールドの基本的な概念のいくつかを、次の図に示します。

3.1. プロバイダー

Springは、OAuth2.0で保護されたリソースの公開を担当するOAuth2プロバイダーの役割を定義します。

この例では、認証サービスがプロバイダー機能を提供するサービスになります。

3.2. クライアント登録

ClientRegistration は、OAuth2(またはOpenID)プロバイダーに登録されている特定のクライアントのすべての関連情報を含むエンティティです。

このシナリオでは、認証サーバーに登録されているクライアントであり、 bael-client-ididで識別されます。

3.3. 認定クライアント

エンドユーザー(別名リソース所有者)がクライアントにリソースへのアクセス許可を付与すると、OAuth2AuthorizedClientエンティティが作成されます。

アクセストークンをクライアント登録およびリソース所有者(プリンシパルオブジェクトで表される)に関連付ける役割を果たします。

3.4. リポジトリ

さらに、Spring Securityは、上記のエンティティにアクセスするためのリポジトリクラスも提供しています。

特に、ReactiveClientRegistrationRepositoryクラスとServerOAuth2AuthorizedClientRepositoryクラスはリアクティブスタックで使用され、デフォルトでメモリ内ストレージを使用します。

Spring Boot 2.xは、これらのリポジトリクラスのBeanを作成し、それらをコンテキストに自動的に追加します。

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

Spring Security 5の重要な概念の1つは、リアクティブSecurityWebFilterChainエンティティです。

その名前が示すように、WebFilterオブジェクトのチェーンコレクションを表します。

アプリケーションでOAuth2機能を有効にすると、SpringSecurityはチェーンに2つのフィルターを追加します。

  1. 1つのフィルターは、承認要求( / oauth2 / authentication / {registrationId} URI)に応答するか、ClientAuthorizationRequiredExceptionをスローします。 これには、 ReactionClientRegistrationRepository、への参照が含まれており、ユーザーエージェントをリダイレクトするための承認リクエストの作成を担当します。
  2. 2番目のフィルターは、追加する機能(OAuth2クライアント機能またはOAuth2ログイン機能)によって異なります。 どちらの場合も、このフィルターの主な役割は、 OAuth2AuthorizedClient インスタンスを作成し、ServerOAuth2AuthorizedClientRepository。を使用して保存することです。

3.6. Webクライアント

Webクライアントは、リポジトリへの参照を含むExchangeFilterFunctionで構成されます。

それらを使用してアクセストークンを取得し、リクエストに自動的に追加します。

4. Spring Security 5のサポート–クライアントクレデンシャルフロー

Spring Securityを使用すると、アプリケーションをOAuth2クライアントとして構成できます。

この記事では、 WebClient インスタンスを使用して、最初に「クライアント資格情報」 付与タイプを使用し、次に「認証コード」フローを使用してリソースを取得します。

最初に行う必要があるのは、アクセストークンを取得するために使用するクライアント登録とプロバイダーを構成することです。

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

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フローを使用してリソースを取得するために必要な構成です。

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インスタンスを設定しましょう。

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    oauth.setDefaultClientRegistrationId("bael");
    return WebClient.builder()
      .filter(oauth)
      .build();
}

前述したように、クライアント登録リポジトリは自動的に作成され、SpringBootによってコンテキストに追加されます。

ここで次に注意することは、UnAuthenticatedServerOAuth2AuthorizedClientRepositoryインスタンスを使用していることです。 これは、マシン間の通信であるため、エンドユーザーがプロセスに参加しないという事実によるものです。 最後に、デフォルトでbaelクライアント登録を使用することを述べました。

それ以外の場合は、cronジョブでリクエストを定義するまでに指定する必要があります。

webClient.get()
  .uri("http://localhost:8084/retrieve-resource")
  .attributes(
    ServerOAuth2AuthorizedClientExchangeFilterFunction
      .clientRegistrationId("bael"))
  .retrieve()
  // ...

4.4. テスト

DEBUG ログレベルを有効にしてアプリケーションを実行すると、Springセキュリティが実行している呼び出しを確認できます。

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つのポイントを指定する必要はありません。

リダイレクトエンドポイントは、SpringSecurityによって自動的に作成されます。

デフォルトでは、そのために構成されたURLは / [action] / oauth2 / code / [registrationId]、であり、許可およびログインアクションのみが許可されます(順番に無限ループを回避するため)。

このエンドポイントは以下を担当します。

  • クエリパラメータとして認証コードを受け取る
  • アクセストークンを取得するためにそれを使用する
  • 許可されたクライアントインスタンスの作成
  • ユーザーエージェントを元のエンドポイントにリダイレクトする

5.2. HTTPセキュリティ構成

次に、SecurityWebFilterChain。を構成する必要があります。

最も一般的なシナリオは、Spring SecurityのOAuth2ログイン機能を使用してユーザーを認証し、ユーザーにエンドポイントとリソースへのアクセスを許可することです。

その場合、 ServerHttpSecurity定義にoauth2Loginディレクティブを含めるだけで、アプリケーションがOAuth2クライアントとしても機能します。

@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オプションも有効にしています。 これにより、フレームワークは、SpringSecurityで管理されている現在の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に返送されます( 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 を使用すると、クライアントとの呼び出しにアクセストークンがアプリケーションに含まれることを意味します。

このコマンドを構成から削除する場合は、要求を定義するまでにクライアント登録を明示的に指定する必要があります。

もちろん、1つの方法は、クライアントの資格情報フローで作業するときに以前に行ったように、clientRegistrationIdを使用することです。

Principal を許可されたクライアントに関連付けたため、 @RegisteredOAuth2AuthorizedClientアノテーションを使用してOAuth2AuthorizedClientインスタンスを取得できます。

@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プロパティを定義するときに、 loginoneの代わりにauthorizeactionを使用できます。

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エンドポイントを使用するとどうなるかを確認しましょう。

まず、フォームログインを使用してアプリケーションにログインする必要があります。

その後、アプリケーションは、リソースへのアクセスを許可するために、承認サービスのログインにリダイレクトします。

注:これを行った後、呼び出した元のエンドポイントにリダイレクトする必要があります。 それにもかかわらず、SpringSecurityは代わりにルートパス「/」にリダイレクトしているようです。これはバグのようです。 OAuth2ダンスをトリガーした後の次のリクエストは正常に実行されます。

エンドポイント応答では、今回承認されたクライアントが、bael-userではなくbael-client-idという名前のプリンシパルに関連付けられていることがわかります。は、認証サービス。

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

箱から出して、 Spring 5は、リクエストにBearerトークンヘッダーを簡単に追加するためのOAuth2関連のサービスメソッドを1つだけ提供します。 これはHttpHeaders#setBearerAuthメソッドです。

ここで、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セキュリティにすべてのハードワークを透過的に処理させます。

9. 結論

このチュートリアルでは、アプリケーションをOAuth2クライアントとして設定する方法、特に WebClient を構成および使用して、完全にリアクティブなスタックで保護されたリソースを取得する方法について説明しました。

最後になりましたが、OAuth2仕様に準拠するためにSpring Security5OAuth2メカニズムが内部でどのように動作するかを分析しました。

いつものように、完全な例はGithubで入手できます。