本連載では、以下に示すようなマイクロサービスアーキテクチャのアプリケーション環境を構築しています。
前回は上図に示すバックエンドサブネットに配置することを想定し、ユーザ情報をリソースとして取り扱うマイクロサービスをローカルで実行できるように実装しました。 今回は第3・4回で作成したWebアプリケーションのSpringSecurityを使った実装で、バックエンドのマイクロサービスから取得したユーザリソースを使って認証処理を行うようアプリケーションを修正していきます。 なお、本連載で実際に作成するアプリケーションでは GitHub 上にコミットしています。 以降に記載するソースコードでは、import文など本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。
第3・4回で実装したコンポーネントに対して、以下の通り修正・追加します。アプリケーションコンポーネントのレイヤ構成については、 連載「AWSで実践!基盤・デプロイ自動化」の連載 第4回 で記載したBackendForFrontendアプリケーションと同様のプロジェクト・パッケージ構成とし、 TERAOLUNAのガイドライン 「アプリケーションのレイヤ化」 の考え方に準拠するものとします。
コンポーネント | 追加/修正 | 説明 |
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クラスの記述をすっきりさせることができます。 なお、今回はコメントアウトしていますが、マイクロサービスのAPIバージョンが変更された場合に備えて、Profileアノテーションなどで切り替えるようにするとよいでしょう。
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());
}
}
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 まで。