前回はマイクロサービスの具体的なアーキテクチャの全容と、そのアーキテクチャを実現する上で難しいハードルの一つとなる認証/認可処理の全体像や考え方を解説しました。

それらを踏まえ、今回からは詳細なアーキテクチャや使用するマネージドサービス/ライブラリなどを整理し、実際に実装を進めていきます。

本連載では、段階的にアプリケーションやそれを実行するのに必要な環境を構築していきますが、まずは最終的なアプリケーション構成や使用するサービスのイメージをお見せしておきましょう。下記の図は、主なクライアントになるWebアプリケーションとそれが呼び出すマイクロサービスをピックアップして詳細化したものです。

アプリケーション構成

なぜこうした構成にするのかを簡単に説明します。前回も解説した通り、Webアプリケーションや管理用のアプリケーション、モバイルアプリケーションに加えてサードパーティの外部サービスなど、さまざまなクライアントがマイクロサービスを利用するというのが前提です。

マイクロサービスは、その時々のリクエスト量に応じてスケールアウト/スケールインをして負荷を分散しながらも、正しい権限を持ったクライアントからのリクエストであるかどうかの正当性を確認しなければなりません。基本的に、クライアントはWebアプリケーションだけでなく、モバイルやサードパーティ製サービスから成っており、不特定多数です。そのため、VPCを分け、全て信頼されたネットワークからしかアクセスできないように制御したインターナルなALBを経由し、オートスケーリング設定を施したECSクラスタに配置されたマイクロサービスを呼び出させるように構成します。

このような構成によって、不正アクセスやDDos攻撃といったセキュリティの脅威を可能な限り排除します。そのため、モノリシックなアプリケーション構成と比べて多段化するので、AWS X-Rayを用いてアプリケーションやサービス間の呼び出しを可視化します。リクエスト頻度が高いサービスがアクセスするデータベースは、場合によってはRDSのほかにもDynamoDBのようなスケーラビリティ性を有するものを使用したほうがよいでしょう。

アプリケーション環境の構築やデプロイは迅速性/正確性が要求されるため、CloudFormationを使って構築します。なお、フロントエンドサブネットでElastiCacheを配置しているのは、フロントに配置されたアプリケーションをスケールさせた場合にセッション共有するためです。そのほか、不要なバックエンドのマイクロサービスの呼び出しを避けるためにキャッシュとしても利用します。

今回からは、フロントエンドサブネットにあるマイクロサービスを利用するクライアントとなるWebアプリケーションを実装していきます。このアプリケーションは連載「AWSで実践! 基盤構築・デプロイ自動化」の第4回で示したアプリケーションパッケージ/コンポーネントとほぼ同等に構成していきます。

最初に作成するのは、バックエンドの呼び出しを行わない簡単なWebアプリケーションのサンプルですが、通常Webアプリケーション自体にも認証認可処理が必要なので、これを追加するところから始めましょう。SpringBootアプリケーションで認証認可を行う場合、一般的にはSpringSecurityを使用します。

次回以降の解説で、Webアプリケーションから呼び出すマイクロサービスにもAWS Cognito、およびSpring Securityを使ったOAuth2のアクセストークンによる認可制御を実装するので、まずSpring Securityの基本的な概要や使い方を押さえておきましょう。

Spring Securityの概要

Spring Securityに関するドキュメントは、 Springの公式ガイド以外にもTERASOLUNAのガイドライン「セキュリティ対策」に詳細に説明されていますが、端的に言えば、「SpringFrameworkを用いてWebアプリケーションを作成する場合に認証認可処理を簡単に実装できるフレームワーク」です。

JavaWebアプリケーションの標準的な機能であるサーブレットフィルタを使って、リクエストが処理される前に正当性の検証や不正リクエストのブロックなど一般的なセキュリティ脆弱性を突いた攻撃に対して防御する機能を提供しています。ほかにもパスワードのハッシュ化や暗号化、本連載のメイントピックの一つになるOAuth2を使ったトークン認証などさまざまなセキュリティ対策の処理を実装しています。

セキュリティ脆弱性をついた攻撃に対する防御方法は一般に難解であり、それらの問題に個々で対処するには難易度も高いので、こうしたライブラリを使用するほうが望ましいでしょう。セキュリティ対策の難解性ゆえに、SpringSecurityを扱うこと自体も難しく捉えられがちですが、非常に拡張性も高く、少量のコーディングで多様なセキュリティ対策処理を実装することが可能です。

前回はOIDCやOAuth2.0を用いた認証認可について説明していますが、今回は先にWebアプリケーションで実装される認証/認可処理を取り上げます。前回も説明した通り、OIDCやOAuth2.0は、あるアプリケーションの一部の処理をWebサービスとして公開したい場合に、不特定多数のクライアントやサードパーティサービスが適切な権限を持ってサービスを利用できるようにするための認証/認可の仕組みです。SpringSecurityでは、どちらの用途もカバーしています。

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

では早速、SpringSecurityを使ってIDとパスワードを使ってログイン/認証を行う簡単なアプリケーションを実装してみましょう。

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

なお、動作環境は以下のバージョンで実施しています。

動作対象 バージョン
Java 11
Spring Boot 2.3.3.RELEASE
Spring Security 5.3.4.RELEASE

まず、使用するライブラリを以下の通り、「pom.xml」に定義します。SpringBootを使ったWebアプリケーションを作成するための「spring-boot-starter-web」、SpringSecurityを使用するための「spring-boot-starter-security」、およびテンプレートエンジンであるThymeleafを使用するための「spring-boot-starter-thymeleaf」を依存定義します。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

それでは、アプリケーションの実装に進みます。今回作成するアプリケーションの構成は以下の通りです。

コンポーネント 説明 必須
WebApp SpringBootアプリケーションを実行する起動クラス
MvcConfig SpringMVCの設定を行うクラス
SecurityConfig SpringSecurity設定クラス
SampleController ログイン画面やログイン後にポータル画面へ遷移するよう定義したController
CustomUserDetails SpringSecurityでユーザ情報を表すモデルオブジェクトを継承したカスタムクラス
CustomUserDetailsServie CustomUserDetailsを取得するためのカスタムクラス
LoginSuccessHandler ログインが成功したのちに実行されるハンドラクラス
SessionExpiredDetectingLoginUrlAuthenticationEntryPoint セッションが無効になったことを検出し、ログイン画面へ遷移するためのハンドラクラス

SpringSecurityを使った認証をアプリケーションに組み込むには、設定クラスを実装して、アプリケーション起動クラスの読み込み設定対象に加える必要があります。今回は以下のような設定クラスを実装し、起動クラスと同一パッケージに配置しておきます。

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;

import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web.security.CustomUserDetailsService;
import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web.security.LoginSuccessHandler;
import org.debugroom.mynavi.sample.aws.microservice.frontend.webapp.app.web.security.SessionExpiredDetectingLoginUrlAuthenticationEntryPoint;

@EnableWebSecurity                                                           //(A)
public class SecurityConfig extends WebSecurityConfigurerAdapter {           //(B)

    // omit

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/static/**");                            //(C)
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
             .antMatchers("/favicon.ico").permitAll()                        //(D)
             .antMatchers("/webjars/**").permitAll()
             .antMatchers("/static/**").permitAll()
             .antMatchers("/timeout").permitAll()
             .anyRequest().authenticated()                                   //(E)
             .and()
             .csrf().disable()                                               //(F)
             .formLogin()
             .loginProcessingUrl("/authenticate")                            //(G)
             .loginPage("/login")                                            //(H)
             .successHandler(loginSuccessHandler())                          //(I)
             .failureUrl("/login")                                           //(J)
             .usernameParameter("username")                                  //(K)
             .passwordParameter("password")                                  //(L)
             .permitAll()                                                    //(M)
             .and()
             .exceptionHandling()
             .authenticationEntryPoint(authenticationEntryPoint())           //(N)
             .and()
             .logout()                                                       //(O)
             .logoutSuccessUrl("/login")
             .permitAll();
    }

    @Bean
    public LoginSuccessHandler loginSuccessHandler(){                        //(P)
        return new LoginSuccessHandler();
    }

    @Bean
    AuthenticationEntryPoint authenticationEntryPoint() {                    //(Q)
        return new SessionExpiredDetectingLoginUrlAuthenticationEntryPoint("/login");
    }

    @Bean
    public PasswordEncoder passwordEncoder(){                                //(R)
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected UserDetailsService userDetailsService() {                      //(S)
        return new CustomUserDetailsService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth
             .userDetailsService(userDetailsService())
             .passwordEncoder(passwordEncoder);                              //(T)
    }
}

SecurityConfigクラスコードの説明は以下の通りです。

項番 説明
A @EnalbleWebSecurityアノテーションを設定クラスへ付与します。このアノテーションにより、SpringSecurityの設定クラスとして認識されます
B SpringSecurityの設定クラスとなるクラスにはorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapterを継承します。継承したクラスの設定用のメソッドをオーバーライドすることでSpringSecurityの基本設定やカスタマイズを行います
C 静的リソースであるCSSやJavaScriptなどsrc/main/resources/static配下に置いていますが、これらのリソースに対するリクエストは対象外とするよう設定します
D セキュリティ設定はビルダークラスであるHttpSecurityに対して、SpringSecurityのチェック対象外とするパスをpermitAll()で設定し、これらのリソースに対するリクエストは対象外とするよう設定します
E D以外のリクエストを全て認証が必要とするよう設定します
F 便宜上、CSRFトークンの送信が必須になる設定をオフにしておきます。この設定については次回以降、改めて有効化させます
G ログイン処理を行うリクエストURIのパスを設定します
H ログインフォームを表示するページを設定します
I ログインが成功したときに実行するハンドラクラスを設定します。この部分については次回解説します
J ログインが失敗したときに遷移するページを設定します。ここではHと同じくログインフォームのページに遷移させます。ユーザー側からは同じページなので遷移しているようには見えません。ただし、エラーメッセージが表示されるように設定します
K ログイン処理でIDに相当するリクエストパラメータを規定します。ここではデフォルトと同じく「username」を設定します。このパラメータはHのログインフォームのパラメータと一致させておく必要があります
L ログイン処理でパスワードに相当するリクエストパラメータを規定します。ここではデフォルトと同じく「password」を設定します。このパラメータはHのログインフォームのパラメータと一致させておく必要があります
M 認証が必要とする設定からログイン処理を除外するように設定します
N 認証処理で例外が発生した場合のハンドリングする設定をauthenticationEntryPointへ行います。詳細は次回以降解説します
O ログアウト時の挙動を設定します。Hと同じく、ログアウトが成功したら遷移する画面をログイン画面に設定します
P ログインが成功したときの処理をカスタムクラスLoginSuccessHanlerとして実装し、Bean定義します。このカスタムクラスについては次回解説します
Q 認証処理で例外が発生した場合の処理をカスタムクラスSessionExpiredDetectingLoginUrlAuthenticationEntryPointとして実装し、Bean定義します。このカスタムクラスについては次回解説します
R パスワードのエンコーダクラスをBean定義します
S 認証情報となるUserDetailsを取得するためのカスタムUserDetailsServiceクラスをBean定義します。このカスタムクラスについては次回解説します
T 認証の設定を行うAuthenticationManagerBuilderクラスにUserDetailsServiceやPasswordEncoderを設定します

今回は、マイクロサービス、およびクライアントとなるWebアプリケーションのアーキテクチャの詳細を説明し、SpringSecurityを使ったWebアプリケーションの設定クラスを実装しました。次回は引き続き、SpringSecutiyを使ったWebアプリケーションログインページの実装やカスタム設定について解説を行い、実際にアプリケーションを起動してみます。

著者紹介


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

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

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

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