本連載では、以下のイメージのようにOAuth2 Loginをベースとしたアーキテクチャを想定した環境構築を進めています。

前回は、Spring Securityを使って、FrontendのアプリケーションでCognitoを認証プロバイダとするOAuth2 Loginを実装しました(上図の1~8の処理に相当)。今回からは、ログイン時に取得したアクセストークンを使って、バックエンドサービスへのアクセス制御を実現する実装を紹介します。上記の図で言うと、0と9の部分の実装方法になります。

なお、実際のソースコードはGitHub上にコミットしています。以降のソースコードでは、本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

バックエンドサービスの呼び出しにアクセストークンを設定する実装

前回解説した通り、Spring Securityでは、OAuth2 Loginが成功した後、Controllerの引数として、org.springframework.security.oauth2.core.oidc.user.OidcUserや org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationTokenを受け取ることができます。このオブジェクトの中には、IDトークン、アクセストークン、リフレッシュトークンの他に、OIDC(OpenID Connect)で定められた ユーザークレイム(ユーザー情報)が含まれており、アプリケーション内でこれらを自由に利用することができます。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web;

// omit

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web.security.CustomUserDetails;

@Controller
public class SampleController {

    @Autowired
    OAuth2AuthorizedClientService oAuth2AuthorizedClientService;

    // omit

    @GetMapping(value = "/oauth2LoginSuccess")
    public String oauth2SuccessPortal(@AuthenticationPrincipal OidcUser oidcUser
                      , OAuth2AuthenticationToken oAuth2AuthenticationToken, Model model){
        OAuth2AuthorizedClient oAuth2AuthorizedClient =
          oAuth2AuthorizedClientService.loadAuthorizedClient(
                  oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(),
                  oAuth2AuthenticationToken.getName());
        model.addAttribute("oidcUser", oidcUser);
        model.addAttribute( oAuth2AuthorizedClientService
          .loadAuthorizedClient(
                  oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(),
                  oAuth2AuthenticationToken.getName()));
        model.addAttribute("accessToken", oAuth2AuthorizedClientService
          .loadAuthorizedClient(
                  oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(),
                  oAuth2AuthenticationToken.getName()).getAccessToken());
        return "oauth2Portal";
   }
}

Spring Securityには、バックエンドサービスなどのリクエストを呼び出す際に、取得したこれらのトークンをリクエストヘッダへ透過的に設定する仕組みが用意されています。 第6回でも解説した通り、このアプリケーションではバックエンドサービスの呼び出しにWebClientを使用しています。このWebClientを使ったサービス呼び出し時にリクエストヘッダへアクセストークンを設定するには、以下のように、設定クラスでWebClientをBean定義する際にorg.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunctionを設定します。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.config;

// omit

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.domain.ServiceProperties;

@Profile("dev")
@Configuration
public class DevConfig {

    @Autowired
    ServiceProperties serviceProperties;

    @Bean
    public WebClient userWebClient(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager){
        ServletOAuth2AuthorizedClientExchangeFilterFunction function =
          new ServletOAuth2AuthorizedClientExchangeFilterFunction(oAuth2AuthorizedClientManager);
        function.setDefaultClientRegistrationId("cognito");
        return WebClient.builder()
                  .baseUrl(serviceProperties.getApplicationLoadBalancer().getDns())
                  .filter(function)
                  .build();
    }

WebClientには、リクエスト呼び出し時に共通する処理を実行するための仕組みとして「ExchageFilterFunction」があります。 Spring Securityで提供されている「ServletOAuth2AuthorizedClientExchangeFilterFunction」を設定することにより、 HTTPリクエストのAuthrozationヘッダに「Bearer : XXXXXX」の形式でアクセストークンをセットするようになります。

なお、設定時には「ClientRegistrationId」を指定しておきます。これは、複数の認証プロバイダをサポートする際に、どのプロバイダのトークンをセットするのか識別するためです。

WebClientを使って実際にバックエンドサービスを呼び出す際は、特にこれまでと変わらず実装して構いません。

package org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.domain.repository;

// omit
import org.springframework.web.reactive.function.client.WebClient;
import org.debugroom.mynavi.sample.aws.microservice.common.model.UserResource;

@Component
public class UserResourceRepositoryImpl implements UserResourceRepository{

    private static final String SERVICE_NAME = "/backend/user";
    private static final String API_VERSION = "/api/v1";

    @Autowired
    WebClient webClient;

    // omit

    @Override
    public UserResource findOne(String userId) {
        String endpoint = SERVICE_NAME + API_VERSION + "/users/{userId}";
        return webClient.get()
          .uri(uriBuilder -> uriBuilder.path(endpoint).build(userId))
          .retrieve()
          .bodyToMono(UserResource.class)
          .block();
    }

今回は、バックエンドサービスを呼び出す際、リクエストヘッダにアクセストークンを設定する方法を解説しました。次回は、バックエンドサービスでアクセストークンによってサービスを保護する実装方法について解説します。