本連載では、以下に示すようなマイクロサービスアーキテクチャのアプリケーション環境を構築しています。
前回は上図に示すバックエンドサブネットに配置することを想定し、ユーザー情報をリソースとして取り扱うマイクロサービスをローカルで実行できるように実装しました。
今回は第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 まで。