本連載では、以下に示すようなマイクロサービスアーキテクチャのアプリケーション環境を構築しています。

構成図

前回は上図に示すバックエンドサブネットに配置することを想定し、ユーザー情報をリソースとして取り扱うマイクロサービスをローカルで実行できるように実装しました。

今回は第3、4回でSpringSecurityを使って実装したWebアプリケーションを修正し、バックエンドのマイクロサービスから取得したユーザーリソースを使って認証処理を行うようにします。

なお、本連載で実際に作成するアプリケーションはGitHub上にコミットしています。ただし、前回の実装とはブランチを変更しているので、参照する際は留意してください。以降に記載するソースコードでは、import文など本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

Spring Securityを使ったWebアプリケーションの修正(1)

第3、4回で実装したコンポーネントに対して、以下のように追加/修正を行っていきます。

コンポーネント 追加/修正 説明
WebApp 変更なし SpringBootアプリケーションを実行する起動クラス
MvcConfig 修正 ユーザーの権限に応じてメニューを切り替えるインターセプタ設定定義を追加するように修正する
SecurityConfig 修正 パスワードエンコーダをBcryptPasswordEncoderを使用するように設定を修正する。 BCryptを使った方法についてはTERASOLUNAガイドライン「BCryptPasswordEncoder」を参照されたい
DevConfig 追加 ローカルで動作する「dev」プロファイルを持つアプリケーションがバックエンドマイクロサービスにアクセスするためのドメインを設定するクラス
DomainConfig 追加 ServiceやRepositoryなどドメイン層の設定クラス
SampleController 修正 CustomUserDetailsを引数のパラメータとして受け取り、画面へモデルとして渡すように修正する
CustomUserDetails 修正 バックエンドマイクロサービスから取得したUserResourceの情報を使ってIDとパスワードを返却するように修正する
CustomUserDetailsServie 修正 バックエンドマイクロサービスからUserResourceを取得し、CustomuUserDetailsにセットして生成、返却するよう修正する
LoginSuccessHandler 変更なし ログインが成功したのちに実行されるハンドラクラス
SessionExpiredDetectingLoginUrlAuthenticationEntryPoint 変更なし セッションが無効になったことを検出し、ログイン画面へ遷移するためのハンドラクラス
Menu 追加 メニューを表すモデルクラス。ポータル、ログアウト、ユーザー管理をEnum型として表す
SetMenuInterceptor 追加 CustomUserDetailsが保持するGrantedAuthoriyに応じて画面に表示させるメニューリストを生成するインターセプタ
PortalInformation 追加 ポータル画面に表示するデータを集約するモデルクラス
ServiceProperties 追加 バックエンドマイクロサービスのドメインを保持するプロパティクラス。application-dev.ymlから取得するよう設定する
UserResourceRepository(Impl) 追加 バックエンドマイクロサービスにアクセスするためのRepositoryクラス。例外処理など集約するためRepositoryクラスを作成し、WebClientを使ったアクセスクラスを実装する
OrchestrationService(Impl) 追加 複数のマイクロサービスへのアクセスを実行制御するためのServiceクラス。Webアプリケーションのビジネスロジックのトランザクション境界として実装され、リトライ制御や補償トランザクションなどの役割を持つ

最新のガイドラインでは、PBKDF2アルゴリズムを使ったパスワード認証方法が推奨されています。

上記の追加/修正のポイントを見ていきましょう。

まず、ドメイン層のコンポーネントとして追加するOrchestrationServiceクラスです。このクラスは、バックエンドのマイクロサービスの呼び出しが複数になる場合などに実行フローを制御する役割を持ち、Webアプリケーションのビジネスロジックのトランザクション境界として実装します。以下の例では、前回実装したバックエンドのユーザーサービスのみを呼び出していますが、必要に応じて別のマイクロサービスの呼び出しや、リトライ制御、エラーが発生してロールバックしたい場合の補償トランザクションなどの処理もこのクラスで行うと良いでしょう。

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

import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.BusinessException;
import org.debugroom.mynavi.sample.aws.microservice.common.model.UserResource;
import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.domain.repository.UserResourceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrchestrationServiceImpl implements OrchestrationService{

    @Autowired
    UserResourceRepository userResourceRepository;

    @Override
    public UserResource getUserResource(String loginId) throws BusinessException {
        return userResourceRepository.findOneByLoginId(loginId);
    }
}

マイクロサービスの呼び出し処理は、Repositoryクラスを作成し、そのなかで実装します。以下の例では、呼び出し処理自体をorg.springframework.web.reactive.function.client.WebClientを使用して行っています。サービス呼び出し時に発生する例外処理を一元的にこのクラスの責務とすることで、上述のOrchestrationServiceクラスの記述をすっきりさせることができます。

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

// omit

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.BusinessException;
import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.BusinessExceptionResponse;
import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.ErrorResponse;
import org.debugroom.mynavi.sample.aws.microservice.common.apinfra.exception.SystemException;
import org.debugroom.mynavi.sample.aws.microservice.common.model.UserResource;

//@Profile("v1")
@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;

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    MessageSource messageSource;

    @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();
    }

    @Override
    public UserResource findOneByLoginId(String loginId) throws BusinessException {
        String endpoint = SERVICE_NAME + API_VERSION + "/users/user";
        try{
            return webClient.get()
              .uri(uriBuilder -> uriBuilder
                      .path(endpoint).queryParam("loginId", loginId).build())
              .retrieve()
              .bodyToMono(UserResource.class)
              .block();
        }catch (WebClientResponseException e){
            try {
                ErrorResponse errorResponse = objectMapper.readValue(
                  e.getResponseBodyAsString(), ErrorResponse.class);
                if(errorResponse instanceof BusinessExceptionResponse){
                    throw ((BusinessExceptionResponse)errorResponse).getBusinessException();
                }else {
                    String errorCode = "SE0002";
                    throw new SystemException(errorCode, messageSource.getMessage(
                      errorCode, new String[]{endpoint}, Locale.getDefault()), e);
                }
            }catch (IOException e1){
                String errorCode = "SE0002";
                throw new SystemException(errorCode, messageSource.getMessage(
                  errorCode, new String[]{endpoint}, Locale.getDefault()), e);
            }
        }
    }

    @Override
    public List<UserResource> findAll() {
        String endpoint = SERVICE_NAME + API_VERSION + "/users";
        return Arrays.asList(
          webClient.get()
                  .uri(uriBuilder -> uriBuilder.path(endpoint).build())
                  .retrieve().bodyToMono(UserResource[].class).block());
    }

}

今回はコメントアウトしていますが、マイクロサービスのAPIバージョンが変更された場合に備えて、Profileアノテーションなどで切り替えるように実装しておくとよいでしょう。

WebClientで呼び出すバックエンドマイクロサービスのURLドメインは、以下のように設定クラスで一律に設定することが可能です。devとなる開発環境固有のapplication-dev.ymlからConfigurationPropertiesなどで取得できるように実装しておきましょう。

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

// omit

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

    @Autowired
    ServiceProperties serviceProperties;

    @Bean
    public WebClient userWebClient(){
        return WebClient.builder()
          .baseUrl(serviceProperties.getDns())
          .build();
    }

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

import org.springframework.boot.context.properties.ConfigurationProperties;
// omit

@ConfigurationProperties(prefix = "service")
public class ServiceProperties {

    private String dns;

}
service:
  dns: http://localhost:8081

なお、本連載ではこれまで、REST APIの呼び出しはRestTemplateを使って実装してきましたが、Spring5.0からメンテナンスモードに移行したため、より通信のコストパフォーマンスが良いWebClientを使った実装に切り替えています。WebClientを使用する際は以下の通り、「spring-boot-starter-webflux」をpom.xmlの依存性に追加する必要があります。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

* * *

今回はバックエンドのユーザーサービスを呼び出すコンポーネントをUserResourceRepositoryとして実装し、OrchestrationService経由で呼び出すように実装しました。次回は、CustomUserDetailsServiceからこれらを呼び出すかたちにして、バックエンドから取得したUserResourceを使って認証処理が行われるように実装し直してみます。

著者紹介


川畑 光平(KAWABATA Kohei) - NTTデータ

金融機関システム業務アプリケーション開発・システム基盤担当、ソフトウェア開発自動化関連の研究開発を経て、デジタル技術関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。