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

構成図

前回は上図に示すマイクロサービスと、クライアントとなるWebアプリケーションのアーキテクチャの詳細を説明し、SpringSecurityを使ったWebアプリケーションの設定クラスを実装しました。

今回は引き続き、SpringSecutiyを使ったさまざまなカスタム設定やWebアプリケーションのログインページを実装し、実際にアプリケーションを起動してみます。

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

前回実装したSpringSecurity設定クラスにBean定義したカスタムクラスについて解説していきます。

まず、「CustomUserDetailsService」です。SpringSecurityによるログイン処理では、リクエストパラメータとして送信されてきたID/パスワードと一致しているかどうかを検証するロジックが既に実装されて組み込まれています。検証対象となるモデルのオブジェクトには、ID/パスワードがプロパティとして定義されているインタフェースorg.springframework.security.core.userdetails.UserDetailsを実装しておきます。

このモデルオブジェクトを取得するサービスクラスはユーザーが任意にカスタマイズできます。ただし、同クラスにはインタフェースorg.springframework.security.core.userdetails.UserDetailsServiceを実装しておくことが必要です。

今回簡単なサンプルとして、以下のようにCustomUserDetailsServiceを実装しています。このクラスの作成は任意ですが、認証情報のモデルオブジェクトをSpringSecurityのデフォルトのまま利用するケースはないので、必須と考えて良いでしょう。

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

import org.springframework.stereotype.Service;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return CustomUserDetails.builder()
          .authorities(AuthorityUtils.createAuthorityList("ROLE_USER"))
          .build();
    }

}

UserDetailsServiceでは、loadUserByUsername()メソッドを実装する必要があります。同メソッドは、戻り値としてインタフェースUserDetailsを実装したクラスを返却します。ログイン処理におけるIDとパスワードは、この返却されたクラスオブジェクトを使ってSpringSecurityが検証処理を行うわけです。

したがって、このメソッド内で「ユーザーのIDとパスワードに相当する情報を取得し、UserDetailsを実装したクラスオブジェクトを生成して返却する処理」を実装することになります。通常は、データベースなどからキーとなるIDを基にパスワードが格納されているユーザー情報を取得し、UserDetailsクラスを実装したモデルオブジェクトにマッピングして返せば良いのですが、上記の例では、処理を簡略化するために、loadUserByUsername()メソッドの引数として渡される文字列型のIDパラメータに関係なく、UserDetailsクラスを実装したCustomUserDetailsに、認可情報となるAuthorityListを生成/設定して返しています。このメソッドのなかで生成されるCustumUserDetailsの実装は以下の通りです。

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

// omit

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class CustomUserDetails implements UserDetails {                      //(A)

    private final Collection authorities;

    @Override
    public Collection extends GrantedAuthority> getAuthorities() {         //(B)
        return authorities;
    }

    @Override
    public String getPassword() {                                            //(C)
        return "{noop}test";                                                 //(D)
    }

    @Override
    public String getUsername() {                                            //(E)
        return "test";
    }

    @Override
    public boolean isAccountNonExpired() {                                   //(F)
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {                                    //(G)
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {                               //(H)
        return true;
    }

    @Override
    public boolean isEnabled() {                                             //(I)
        return true;
    }

}

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

項番 説明
A org.springframework.security.core.userdetails.UserDetailsを実装します。このインタフェースを実装するとB、C、E、F、G、H、Iのメソッドをオーバーライドする必要があります
B UserDetailsで実装が必要なメソッドです。認可に相当するGrantedAutorityをCollection型で保持して返却します
C UserDetailsで実装が必要なメソッドです。リクエストで送信されるパスワードと一致するかどうかを検証するパスワードを返却します
D このサンプルでは、パスワードを固定値"test"で返します。接頭辞"{noop}"は特にハッシュ化も暗号化せずに処理を行うパスワードエンコーダであるorg.springframework.security.crypto.password.NoOpPasswordEncoderを使用する場合に付与します
E UserDetailsで実装が必要なメソッドです。リクエストで送信されるIDと一致するかどうかを検証するIDを返却します
F UserDetailsで実装が必要なメソッドです。アカウントの期限が有効である場合に「true」を返却するよう実装します(このサンプルでは常にtrueで返却します)
G UserDetailsで実装が必要なメソッドです。アカウントがロックされていない場合に「true」を返却するよう実装します(このサンプルでは常に「true」で返却します)
H UserDetailsで実装が必要なメソッドです。認証情報が有効である場合に「true」を返却するよう実装します(このサンプルでは常に「true」で返却します)
I UserDetailsで実装が必要なメソッドです。このアカウントが有効である場合に「true」を返却するよう実装します(このサンプルでは常に「true」で返却します)

次に実装するのは、ログインが成功した後に実行されるLoginSuccessHanderクラスです。AuthenticationSuccessHandlerを実装し、onAuthenticationSuccess()メソッドを実行しておくと、ログインが成功した後、このハンドラクラスが呼ばれます。以下では、パス”/frontend/portal”へリダイレクトする処理を実装しています。

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

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest
      , HttpServletResponse httpServletResponse
      , Authentication authentication) throws IOException, ServletException {
        httpServletResponse.sendRedirect("/frontend/portal");
    }

}

ログイン後のリダイレクトは、設定クラス内のフォーム認証の設定でderaultSuccressUrl()のデフォルトのページを指定することもできます。「ユーザーの権限ごとに異なるページに遷移させたい」「ログイン時間を記録したい」といった、より高度なカスタマイズ処理を加えたい場合に利用するとよいでしょう。

エラー処理のカスタマイズ

続いて、認証処理で何らかのエラーが発生した場合の処理をカスタマイズしたい場合に利用するクラスを紹介します。

以下に示すクラスSessionExpiredDetectingLoginUrlAuthenticationEntryPointは、セッションが無効になった場合にそれを検出し、タイムアウト画面へ遷移させるためのカスタムクラスです。通常、エラーが発生した際は、org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPointに設定されたloginFormUrlに基づいてリダイレクトされますが、その処理の過程でセッションの有効状態を検証し、無効な状態になっているようであれば、別の画面へリダイレクトさせます。カスタマイズにあたっては、LoginUrlAuthenticationEntryPointを継承して、リダイレクトURLを生成する箇所をオーバーライドしています。

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

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

public class SessionExpiredDetectingLoginUrlAuthenticationEntryPoint
     extends LoginUrlAuthenticationEntryPoint {

    public SessionExpiredDetectingLoginUrlAuthenticationEntryPoint(
         String loginFormUrl) {
        super(loginFormUrl);
    }

    @Override
    protected String buildRedirectUrlToLoginPage(HttpServletRequest request
      , HttpServletResponse response, AuthenticationException authException) {
        String redirectUrl = super.buildRedirectUrlToLoginPage(
          request, response, authException);
        if(isRequestedSessionInvalid(request)){
            redirectUrl = "timeout";
        }
        return redirectUrl;
    }

    private boolean isRequestedSessionInvalid(HttpServletRequest request){
        return Objects.nonNull(request.getRequestedSessionId())
          && !request.isRequestedSessionIdValid();
    }
}

このようにSpringSecurityではアプリケーションの認証/認可を行う処理の実装の大部分を用意しながらも、要件に応じたカスタマイズができるようにカスタムエントリポイントとなるクラスやメソッドを多数提供しています。

最後に、ログイン画面やログイン成功後に遷移するポータル画面を実装しましょう。ポイントとなるのはログイン画面でエラーが発生した場合の実装です。ログイン成功後と同じ画面に遷移しますが、エラーメッセージが表示されるようにしておくことと、ログイン処理を行うURLのパスやフォームのID/パスワードのリクエストパラメータ名をSpringSecurity設定クラスで指定したものと合わせておくことに留意してください。なお、ポータル画面は特に説明すべきことはないので割愛します。

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" lang="ja">
<!-- omit -->
<body>
<h1>Mynavi Microservice WebApp Login</h1>

<!-- エラーが発生した場合にエラーメッセージを表示させる -->
<div th:if="${param.containsKey('error')}"
   th:with="exception = ${SPRING_SECURITY_LAST_EXCEPTION} ?: ${session[SPRING_SECURITY_LAST_EXCEPTION]}">
   <ul th:if="${exception != null}" class="alert alert-error">
     <li th:text="${exception.message}"></li>
   </ul>
</div>

<!-- ログインフォーム:ログイン処理を行うURLパスやフォームのIDとパスワードのリクエストパラメータ名を設定クラスのものと合わせておく。 -->
<form th:action="@{/authenticate}" method="post">
  <table>
    <tr>
      <td><label for="username">User:</label></td>
      <td><input type="text" id="username" name="username" placeholder="LoginId" value="test">(demo)</td>
    </tr>
    <tr>
      <td><label for="password">Password:</label></td>
      <td><input type="password" id="password" name="password" placeholder="Password" value="test">(demo)</td>
    </tr>
    <tr>
      <td> </td>
      <td><input name="submit" type="submit" value="Login"></td>
    </tr>
  </table>
</form>
</body>

起動クラスを実行し、アプリケーションを起動してログインすると、ポータル画面へ遷移します。ログイン後はセッションが有効になるので、セッションIDを表示させています。

ログイン
セッションID

今回は、SpringSecutiyを使ったWebアプリケーションのログインページの実装やカスタム設定について解説し、実際にアプリケーションを起動してみました。次回以降は、RDBに保存したユーザー情報を取得するバックエンドマイクロサービスを作成し、今回作成したCustomUserDetailsServiceから呼び出すようにして、RDBに保存された情報を使って正しく認証処理が行われるように実装し直してみます。併せて、AWS X-Rayを使ってサービス呼び出しを可視化する方法も紹介する予定です。

著者紹介


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

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

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

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