9.2. 認証


9.2.1. Overview

本節では、Spring Securityが提供している認証機能について説明する。

認証処理は、アプリケーションを利用するユーザーの正当性を確認するための処理である。

ユーザーの正当性を確認するためのもっとも標準的な方法は、アプリケーションを使用できるユーザーをデータストアに登録しておき、利用者が入力した認証情報(ユーザー名とパスワードなど)と照合する方法である。
ユーザーの情報を登録しておくデータストアにはリレーショナルデータベースを利用するのが一般的だが、ディレクトリサービスや外部システムなどを利用するケースもある。
また、利用者に認証情報を入力してもらう方式もいくつか存在する。
HTMLの入力フォームを使う方式やRFCで定められているHTTP標準の認証方式(Basic認証やDigest認証など)を利用するのが一般的だが、OpenID認証やシングルサインオン認証などの認証方式を利用するケースもある。
本節では、HTMLの入力フォームで入力した認証情報とリレーショナルデータベースに格納されているユーザー情報を照合して認証処理を行う実装例を紹介しながら、Spring Securityの認証機能の使い方を説明する。

Note

ブランクプロジェクトではデフォルトでHttpSecurity#formLogin(<sec:form-login/>タグ)とHttpSecurity#logout(<sec:logout/>タグ)が設定されており、Spring Securityのフォーム認証が有効となっている。

フォーム認証を使用しない場合は、これらのタグを削除する必要がある。削除しない場合、ユーザから/login /logoutに対するリクエストがあると、想定外の認証処理が実行される可能性がある。


9.2.1.1. 認証処理のアーキテクチャ

Spring Securityは、以下のような流れで認証処理を行う。

../_images/AuthenticationArchitecture.png

認証処理のアーキテクチャ

項番

説明

(1)
クライアントは、認証処理を行うパスに対して資格情報(ユーザー名とパスワード)を指定してリクエストを送信する。
(2)
Authentication Filterは、リクエストから資格情報を取得して、AuthenticationManagerクラスの認証処理を呼び出す。
(3)
ProviderManager(デフォルトで使用されるAuthenticationManagerの実装クラス)は、実際の認証処理をAuthenticationProviderインタフェースの実装クラスに委譲する。

9.2.1.1.1. Authentication Filter

Authentication Filterは、認証方式に対する実装を提供するサーブレットフィルタである。
Spring Securityがサポートしている主な認証方式は以下の通り。
Spring Securityが提供している主なAuthentication Filter

クラス名

説明

UsernamePasswordAuthenticationFilter
フォーム認証用のサーブレットフィルタクラスで、HTTPリクエストのパラメータから資格情報を取得する。
BasicAuthenticationFilter
Basic認証用のサーブレットフィルタクラスで、HTTPリクエストの認証ヘッダから資格情報を取得する。
DigestAuthenticationFilter
Digest認証用のサーブレットフィルタクラスで、HTTPリクエストの認証ヘッダから資格情報を取得する。
RememberMeAuthenticationFilter
Remember Me認証用のサーブレットフィルタクラスで、HTTPリクエストのCookieから資格情報を取得する。
Remember Me認証を有効にすると、ブラウザを閉じたりセッションタイムアウトが発生しても、ログイン状態を保つことができる。

これらのサーブレットフィルタは、 フレームワーク処理で紹介したAuthentication Filterの1つである。

Note

Spring Securityによってサポートされていない認証方式を実現する必要がある場合は、認証方式を実現するためのAuthentication Filterを作成し、Spring Securityに組み込むことで実現することが可能である。


9.2.1.1.2. AuthenticationManager

AuthenticationManagerは、認証処理を実行するためのインタフェースである。
Spring Securityが提供するデフォルト実装(ProviderManager)では、実際の認証処理はAuthenticationProviderに委譲し、AuthenticationProviderで行われた認証処理の処理結果をハンドリングする仕組みになっている。

9.2.1.1.3. AuthenticationProvider

AuthenticationProviderは、認証処理の実装を提供するためのインタフェースである。
Spring Securityが提供している主なAuthenticationProviderの実装クラスは以下の通り。
Spring Securityが提供している主なAuthenticationProvider

クラス名

説明

DaoAuthenticationProvider
データストアに登録しているユーザーの資格情報とユーザーの状態をチェックして認証処理を行う実装クラス。
チェックで必要となる資格情報とユーザーの状態はUserDetailsというインタフェースを実装しているクラスから取得する。
RememberMeAuthenticationProvider
Remember Me認証用のTokenを検証するAuthenticationProviderの実装クラス。

Note

Spring Securityが提供していない認証処理を実現する必要がある場合は、認証処理を実現するためのAuthenticationProviderを作成し、Spring Securityに組み込むことで実現することが可能である。


9.2.2. How to use

認証機能を使用するために必要となるbean定義例や実装方法について説明する。

本項ではOverviewで説明したとおり、HTMLの入力フォームで入力した認証情報とリレーショナルデータベースに格納されているユーザー情報を照合して認証処理を行う方法について説明する。


9.2.2.1. フォーム認証

Spring Securityは、以下のような流れでフォーム認証を行う。

../_images/AuthenticationForm.png

フォーム認証の仕組み

項番

説明

(1)
クライアントは、フォーム認証を行うパスに対して資格情報(ユーザー名とパスワード)をリクエストパラメータとして送信する。
(2)
UsernamePasswordAuthenticationFilterクラスは、リクエストパラメータから資格情報を取得して、AuthenticationManagerの認証処理を呼び出す。
(3)
UsernamePasswordAuthenticationFilterクラスは、AuthenticationManagerから返却された認証結果をハンドリングする。
認証処理が成功した場合は AuthenticationSuccessHandlerのメソッドを呼び出し、認証処理が失敗した場合はAuthenticationFailureHandlerのメソッドを呼び出し、画面遷移を行う。

9.2.2.1.1. フォーム認証の適用

フォーム認証を使用する場合は、以下のようなbean定義を行う。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http.formLogin(Customizer.withDefaults()); // (1)
    // omitted
    return http.build();
}

項番

説明

(1)
HttpSecurity#formLoginを呼び出すことで、フォーム認証が有効になる。

Tip

auto-config属性について

Java Config には、XML Config で設定可能なauto-config=trueに対応する設定は存在していない。

auto-config=trueに対応する設定は、フォーム認証(HttpSecurity#formLogin)、Basic認証(HttpSecurity#httpBasic)、ログアウト(HttpSecurity#logout)のデフォルト値の使用と同等となる。

要素名

説明

formLogin(Customizer.withDefaults())
フォーム認証処理を行うSecurity Filter(UsernamePasswordAuthenticationFilter)が適用される。
httpBasic(Customizer.withDefaults())
RFC1945に準拠したBasic認証を行うSecurity Filter(BasicAuthenticationFilter)が適用される。
詳細な利用方法は、BasicAuthenticationFilterのJavaDocを参照されたい。
logout(Customizer.withDefaults())
ログアウト処理を行うSecurity Filter(LogoutFilter)が適用される。
ログアウト処理の詳細については、「ログアウト」を参照されたい。

なお、フォーム認証(HttpSecurity#formLogin)、もしくはBasic認証(HttpSecurity#httpBasic)は必ず定義する必要がある。これは、ひとつのSecurityFilterChain内には、ひとつ以上のAuthentication FilterのBean定義が必要であるという、Spring Securityの仕様を満たすためである。


9.2.2.1.2. デフォルトの動作

Spring Securityのデフォルトの動作では、/loginに対してGETメソッドでアクセスするとSpring Securityが用意しているデフォルトのログインフォームが表示され、ログインボタンを押下すると/loginに対してPOSTメソッドでアクセスして認証処理を行う。


9.2.2.1.3. ログインフォームの作成

Spring Securityはフォーム認証用のログインフォームをデフォルトで提供しているが、そのまま利用するケースは少ない。
ここでは、自身で作成したログインフォームをSpring Securityに適用する方法を説明する。
まず、ログインフォームを表示するためのViewを作成する。
ここでは、Spring MVCでリクエストをうけてログインフォームを表示する際の実装例になっている。
  • xxx-web/src/main/webapp/WEB-INF/views/login/loginForm.jsp

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%-- omitted --%>
<div id="wrapper">
    <h3>Login Screen</h3>
    <%-- (1) --%>
    <c:if test="${param.containsKey('error')}">
        <t:messagesPanel messagesType="error"
            messagesAttributeName="SPRING_SECURITY_LAST_EXCEPTION"/> <%-- (2) --%>
    </c:if>
    <form:form action="${pageContext.request.contextPath}/login" method="post"> <%-- (3) --%>
        <table>
            <tr>
                <td><label for="username">User Name</label></td>
                <td><input type="text" id="username" name="username"></td>
            </tr>
            <tr>
                <td><label for="password">Password</label></td>
                <td><input type="password" id="password" name="password"></td>
            </tr>
            <tr>
                <td>&nbsp;</td>
                <td><button>Login</button></td>
            </tr>
        </table>
    </form:form>
</div>
<%-- omitted --%>

項番

説明

(1)
認証エラーを表示するためのエリア。
(2)
認証エラー時に出力させる例外メッセージを出力する。
共通ライブラリで提供している<t:messagesPanel>タグを使用して出力することを推奨する。
<t:messagesPanel>タグの使用方法については、「メッセージ管理」を参照されたい。
なお、認証エラーが発生した場合、Spring Securityのデフォルトの設定で使用される、org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandlerでは、認証エラー時に発生した例外オブジェクトをSPRING_SECURITY_LAST_EXCEPTIONという属性名で、リダイレクト時はセッション、フォワード時はリクエストスコープに格納する。
(3)
ユーザー名とパスワードを入力するためのログインフォーム。
ここではユーザー名をusername、パスワードをpassowrdというリクエストパラメータで送信する。
また、<form:form>を使用することで、CSRF対策用のトークン値がリクエストパラメータで送信される。
CSRF対策については、「CSRF対策」で説明する。

つぎに、作成したログインフォームをSpring Securityに適用する。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    http.formLogin(login -> login.loginPage("/login/loginForm") // (1)
            .loginProcessingUrl("/login")); // (2)
    http.authorizeHttpRequests(authz -> authz
            .requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll() // (3)
            .requestMatchers(new AntPathRequestMatcher("/**")).authenticated()); // (4)

    return http.build();
}

項番

説明

(1)
loginPageにログインフォームを表示するためのパスを指定する。
匿名ユーザーが認証を必要とするWebリソースにアクセスした場合は、この属性に指定したパスにリダイレクトしてログインフォームを表示する。
ここでは、Spring MVCでリクエストを受けてログインフォームを表示している。
(2)
loginProcessingUrlに認証処理を行うためのパスを指定する。
デフォルトのパスも/loginであるが、ここでは明示的に指定することとする。
(3)
ログインフォームが格納されている/loginパス配下に対し、すべてのユーザーがアクセスできる権限を付与する。
Webリソースに対してアクセスポリシーの指定方法については、「認可」を参照されたい。
(4)
アプリケーションで扱うWebリソースに対してアクセス権を付与する。
上記例では、Webアプリケーションのルートパスの配下に対して、認証済みユーザーのみがアクセスできる権限を付与している。
Webリソースに対してアクセスポリシーの指定方法については、「認可」を参照されたい。

9.2.2.2. 認証成功時のレスポンス

Spring Securityは、認証成功時のレスポンスを制御するためのコンポーネントとして、AuthenticationSuccessHandlerというインタフェースと実装クラスを提供している。

主なAuthenticationSuccessHandlerの実装クラス

実装クラス

説明

SavedRequestAwareAuthenticationSuccessHandler
認証前にアクセスを試みたURLにリダイレクトを行う実装クラス。
デフォルトで使用される実装クラス。
SimpleUrlAuthenticationSuccessHandler
defaultTargetUrlにリダイレクト又はフォワードを行う実装クラス。

9.2.2.2.1. デフォルトの動作

Spring Securityのデフォルトの動作では、認証前にアクセスを拒否したリクエストをHTTPセッションに保存しておいて、認証が成功した際にアクセスを拒否したリクエストを復元してリダイレクトする。
認証したユーザーにリダイレクト先へのアクセス権があればページが表示され、アクセス権がなければ認可エラーとなる。
この動作を実現するために使用されるのが、SavedRequestAwareAuthenticationSuccessHandlerクラスである。
ログインフォームを明示的に表示してから認証処理を行った後の遷移先は、Spring Securityのデフォルトの設定ではWebアプリケーションのルートパス(”/“ )となっているため、認証成功時はWebアプリケーションのルートパスにリダイレクトされる。

9.2.2.3. 認証失敗時のレスポンス

Spring Securityは、認証失敗時のレスポンスを制御するためのコンポーネントとして、AuthenticationFailureHandlerというインタフェースと実装クラスを提供している。

主なAuthenticationFailureHandlerの実装クラス

実装クラス

説明

SimpleUrlAuthenticationFailureHandler
指定したパス(defaultFailureUrl)にリダイレクト又はフォワードを行う実装クラス。
ExceptionMappingAuthenticationFailureHandler
認証例外と遷移先のURLをマッピングすることができる実装クラス。
Spring Securityはエラー原因毎に発生する例外クラスが異なるため、この実装クラスを使用するとエラーの種類毎に遷移先を切り替えることが可能である。
DelegatingAuthenticationFailureHandler
認証例外とAuthenticationFailureHandlerをマッピングすることができる実装クラス。
ExceptionMappingAuthenticationFailureHandlerと似ているが、認証例外毎にAuthenticationFailureHandlerを指定できるので、より柔軟な振る舞いをサポートすることができる。

9.2.2.3.1. デフォルトの動作

Spring Securityのデフォルトの動作では、ログインフォームを表示するためのパスにerrorというクエリパラメータが付与されたURLにリダイレクトする。

例として、ログインフォームを表示するためのパスが/loginの場合は/login?errorにリダイレクトされる。


9.2.2.4. DB認証

Spring Securityは、以下のような流れでDB認証を行う。

../_images/AuthenticationDatabase.png

DB認証の仕組み

項番

説明

(1)
Spring Securityはクライアントからの認証依頼を受け、DaoAuthenticationProviderの認証処理を呼び出す。
(2)
DaoAuthenticationProviderは、UserDetailsServiceのユーザー情報取得処理を呼び出す。
(3)
UserDetailsServiceの実装クラスは、データストアからユーザー情報を取得する。
(4)
UserDetailsServiceの実装クラスは、データストアから取得したユーザー情報からUserDetailsを生成する。
(5)
DaoAuthenticationProviderは、UserDetailsServiceから返却されたUserDetailsとクライアントが指定した認証情報との照合を行い、クライアントが指定したユーザーの正当性をチェックする。

Note

Spring Securityが提供するDB認証

Spring Securityは、ユーザー情報をリレーショナルデータベースからJDBC経由で取得するための実装クラスを提供している。

  • org.springframework.security.core.userdetails.User(UserDetailsの実装クラス)

  • org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl (UserDetailsServiceの実装クラス)

これらの実装クラスは最低限の認証処理(パスワードの照合、有効ユーザーの判定)しか行わないため、そのまま利用できるケースは少ない。

そのため、本ガイドラインでは、UserDetailsUserDetailsServiceの実装クラスを作成する方法について説明する。


9.2.2.4.1. UserDetailsの作成

UserDetailsは、認証処理で必要となる資格情報(ユーザー名とパスワード)とユーザーの状態を提供するためのインタフェースで、以下のメソッドが定義されている。
AuthenticationProviderとしてDaoAuthenticationProviderを使用する場合は、アプリケーションの要件に合わせてUserDetailsの実装クラスを作成する。

UserDetailsインタフェース

public interface UserDetails extends Serializable {
    String getUsername(); // (1)
    String getPassword(); // (2)
    boolean isEnabled(); // (3)
    boolean isAccountNonLocked(); // (4)
    boolean isAccountNonExpired(); // (5)
    boolean isCredentialsNonExpired(); // (6)
    Collection<? extends GrantedAuthority> getAuthorities(); // (7)
}

項番

メソッド名

説明

(1)
getUsername
ユーザー名を返却する。
(2)
getPassword
登録されているパスワードを返却する。
このメソッドで返却したパスワードとクライアントから指定されたパスワードが一致しない場合は、DaoAuthenticationProviderBadCredentialsExceptionを発生させる。
(3)
isEnabled
有効なユーザーかを判定する。有効な場合はtrueを返却する。
無効なユーザーの場合は、DaoAuthenticationProviderDisabledExceptionを発生させる。
(4)
isAccountNonLocked
アカウントのロック状態を判定する。ロックされていない場合はtrueを返却する。
アカウントがロックされている場合は、DaoAuthenticationProviderLockedExceptionを発生させる。
(5)
isAccountNonExpired
アカウントの有効期限の状態を判定する。有効期限内の場合はtrueを返却する。
有効期限切れの場合は、DaoAuthenticationProviderAccountExpiredExceptionを発生させる。
(6)
isCredentialsNonExpired
資格情報の有効期限の状態を判定する。有効期限内の場合はtrueを返却する。
有効期限切れの場合は、DaoAuthenticationProviderCredentialsExpiredExceptionを発生させる。
(7)
getAuthorities
ユーザーに与えられている権限リストを返却する。
このメソッドは認可処理で使用される。

Note

認証例外による遷移先の切り替え

DaoAuthenticationProviderが発生させる例外毎に画面遷移を切り替えたい場合は、AuthenticationFailureHandlerとしてExceptionMappingAuthenticationFailureHandlerを使用すると実現することができる。

例として、ユーザーのパスワードの有効期限が切れた際にパスワード変更画面に遷移させたい場合は、ExceptionMappingAuthenticationFailureHandlerを使ってCredentialsExpiredExceptionをハンドリングすると画面遷移を切り替えることができる。

詳細は、認証失敗時のレスポンスのカスタマイズを参照されたい。

Note

Spring Securityが提供する資格情報

Spring Securityは、資格情報(ユーザー名とパスワード)とユーザーの状態を保持するための実装クラス(org.springframework.security.core.userdetails.User)を提供しているが、このクラスは認証処理に必要な情報しか保持することができない。

一般的なアプリケーションでは、認証処理で使用しないユーザーの情報(ユーザーの氏名など)も必要になるケースが多いため、Userクラスをそのまま利用できるケースは少ない。


ここでは、アカウントの情報を保持するUserDetailsの実装クラスを作成する。
本例はUserを継承することでも実現することができるが、UserDetails を実装する方法の例として紹介している。
  • UserDetailsの実装クラスの作成例

public class AccountUserDetails implements UserDetails, CredentialsContainer { // (1)

    private final Account account;
    private final Collection<GrantedAuthority> authorities;

    public AccountUserDetails(
        Account account, Collection<GrantedAuthority> authorities) {
        // (2)
        this.account = account;
        this.authorities = authorities;
    }

    // (3)
    public String getPassword() {
        return account.getPassword();
    }
    public String getUsername() {
        return account.getUsername();
    }
    public boolean isEnabled() {
        return account.isEnabled();
    }
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    // (4)
    public boolean isAccountNonExpired() {
        return true;
    }
    public boolean isAccountNonLocked() {
        return true;
    }
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // (5)
    public Account getAccount() {
        return account;
    }

    // (6)
    @Override
    public void eraseCredentials() {
       account.setPassword(null);
    }

  }

項番

説明

(1)
UserDetailsインタフェースとCredentialsContainerインターフェースを実装したクラスを作成する。
(2)
ユーザー情報と権限情報をプロパティに保持する。
(3)
UserDetailsインタフェースに定義されているメソッドを実装する。
(4)
本節の例では、「アカウントのロック」「アカウントの有効期限切れ」「資格情報の有効期限切れ」に対するチェックは未実装であるが、要件に合わせて実装されたい。
(5)
認証処理成功後の処理でアカウント情報にアクセスできるようにするために、getterメソッドを用意する。
(6)
UserDetailsに保持されているパスワードの情報を削除する。
CredentialsContainerを実装することで、認証処理完了時にeraseCredentials()メソッドが呼び出され機密データ(上記例ではパスワード)を削除することができる。

Note

UserDetails実装クラスのequalsメソッドについて

UserDetailsを実装する際に、equalsメソッドを実装しない場合はObjectの比較となる。

そのため、要件によってはequalsメソッドを実装する必要がある。例として、Spring Securityの提供するUserクラスでは、usernameが一致するかを確認している。


Spring Securityは、UserDetailsの実装クラスとしてUserクラスを提供している。
Userクラスを継承すると資格情報とユーザーの状態を簡単に保持することができる。
  • Userクラスを継承したUserDetails実装クラスの作成例

public class AccountUserDetails extends User {

    private final Account account;

    public AccountUserDetails(Account account, boolean accountNonExpired,
            boolean credentialsNonExpired, boolean accountNonLocked,
            Collection<GrantedAuthority> authorities) {
        super(account.getUsername(), account.getPassword(),
                account.isEnabled(), true, true, true, authorities);
        this.account = account;
    }

    public Account getAccount() {
        return account;
    }
}

9.2.2.4.2. UserDetailsServiceの作成

UserDetailsServiceは、認証処理で必要となる資格情報とユーザーの状態をデータストアから取得するためのインタフェースで、以下のメソッドが定義されている。
AuthenticationProviderとしてDaoAuthenticationProviderを使用する場合は、アプリケーションの要件に合わせてUserDetailsServiceの実装クラスを作成する。
  • UserDetailsServiceインタフェース

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

ここでは、データベースからアカウント情報を検索して、UserDetailsのインスタンスを生成するためのサービスクラスを作成する。
本サンプルでは、SharedServiceを使用して、アカウント情報を取得している。
SharedServiceについては、Serviceの実装を参照されたい。
  • AccountSharedServiceインタフェースの作成例

public interface AccountSharedService {
    Account findOne(String username);
}
  • AccountSharedServiceの実装クラスの作成例

// (1)
@Service
@Transactional
public class AccountSharedServiceImpl implements AccountSharedService {
    @Inject
    AccountRepository accountRepository;

    // (2)
    @Override
    public Account findOne(String username) {
        Account account = accountRepository.findByUsername(username);
        if (account == null) {
            throw new ResourceNotFoundException("The given account is not found! username="
                    + username);
        }
        return account;
    }
}

項番

説明

(1)
AccountSharedServiceインタフェースを実装したクラスを作成し、@Serviceを付与する。
上記例では、コンポーネントスキャン機能を使ってAccountSharedServiceImplをDIコンテナに登録している。
(2)
データベースからアカウント情報を検索する。
アカウント情報が見つからない場合は、共通ライブラリの例外であるResourceNotFoundExceptionを発生させる。
Repositoryの作成例については、「Spring Securityチュートリアル」を参照されたい。
  • UserDetailsServiceの実装クラスの作成例

// (1)
@Service
@Transactional
public class AccountUserDetailsService implements UserDetailsService {
    @Inject
    AccountSharedService accountSharedService;

    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        try {
            Account account = accountSharedService.findOne(username);
            // (2)
            return new AccountUserDetails(account, getAuthorities(account));
        } catch (ResourceNotFoundException e) {
            // (3)
            throw new UsernameNotFoundException("user not found", e);
        }
    }

    // (4)
    private Collection<GrantedAuthority> getAuthorities(Account account) {
        if (account.isAdmin()) {
            return AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN");
        } else {
            return AuthorityUtils.createAuthorityList("ROLE_USER");
        }
    }
}

項番

説明

(1)
UserDetailsServiceインタフェースを実装したクラスを作成し、@Serviceを付与する。
上記例では、コンポーネントスキャン機能を使ってUserDetailsServiceをDIコンテナに登録している。
(2)
AccountSharedServiceを使用してアカウント情報を取得する。
アカウント情報が見つかった場合は、UserDetailsを生成する。
上記例では、ユーザー名、パスワード、ユーザーの有効状態をアカウント情報から取得している。
(3)
アカウント情報が見つからない場合は、UsernameNotFoundExceptionを発生させる。
(4)
ユーザーが保持する権限(ロール)情報を生成する。ここで生成した権限(ロール)情報は、認可処理で使用される。

Note

認可で使用する権限情報

Spring Securityの認可処理は、ROLE_で始まる権限情報をロールとして扱う。

そのため、ロールを使用してリソースへのアクセス制御を行う場合は、 ロールとして扱う権限情報にROLE_プレフィックスを付与する必要がある。

Note

認証例外情報の隠蔽

Spring Securityのデフォルトの動作では、UsernameNotFoundExceptionBadCredentialsExceptionという例外に変換してからエラー処理を行う。

BadCredentialsExceptionは、クライアントから指定された資格情報のいずれかの項目に誤りがあることを通知するための例外であり、具体的なエラー理由がクライアントに通知されることはない。


9.2.2.4.3. DB認証の適用

作成したUserDetailsServiceを使用して認証処理を行うためには、DaoAuthenticationProviderを有効化して、作成したUserDetailsServiceを適用する必要がある。

  • SpringSecurityConfig.javaの定義例

    // (1)
    @Bean("dbUserLoginManager")
    public AuthenticationManager dbUserLoginManager() throws Exception {
        return new ProviderManager(authenticationProvider()); //(2)
    }
    
    // (2)
    @Bean("authenticationProvider")
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(accountUserDetailsService());
        return authProvider;
    }
    

    項番

    説明

    (1)
    AuthenticationManagerをbean定義する。
    (2)
    ProviderManagerのコンストラクタ引数にAuthenticationProviderを定義する。
    UserDetailsServiceに「UserDetailsServiceの作成」で作成したAccountUserDetailsServiceのbeanを指定する。
    本定義により、デフォルト設定のDaoAuthenticationProviderが有効になる。

Note

Spring Securityは、passwordEncoderという名前のBeanを定義していると、sec:authentication-provider配下にsec:password-encoder要素を指定しない場合に自動的に参照する。これにより、sec:password-encoderの指定を省略することが可能である。

sec:password-encoder要素を省略し、かつpasswordEncoderという名前のBeanが存在しない場合、org.springframework.security.crypto.factory.PasswordEncoderFactoriesを利用して生成したDelegatingPasswordEncoderが使用される。


9.2.2.5. パスワードのハッシュ化

パスワードをデータベースなどに保存する場合は、パスワードそのものではなくパスワードのハッシュ値を保存するのが一般的である。

Spring Securityは、パスワードをハッシュ化するためのインタフェースと実装クラスを提供しており、認証機能と連携して動作する。

Spring Securityは以下のインタフェースを提供している。

  • org.springframework.security.crypto.password.PasswordEncoder


org.springframework.security.crypto.password.PasswordEncoderのメソッド定義

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
PasswordEncoderに定義されているメソッド

メソッド名

説明

encode
パスワードをハッシュ化するためのメソッド。
アカウントの登録処理やパスワード変更処理などでデータストアに保存するパスワードをハッシュ化する際に使用できる。
matches
平文のパスワードとハッシュ化されたパスワードを照合するためのメソッド。
このメソッドはSpring Securityの認証処理でも利用されるが、パスワード変更処理などで現在のパスワードや過去に使用していたパスワードと照合する際にも使用できる。
upgradeEncoding
ハッシュ化されたパスワードをセキュリティ強化のために再度ハッシュ化する必要があるか検証するためのメソッド。
本メソッドは、DB等から取得したハッシュ化されたパスワードを認証情報として保持する際に、セキュリティ強度の低いハッシュの漏洩を防止するために利用され、主にフレームワーク内部で利用されるメソッドである。

Spring Securityは、PasswordEncoderインタフェースの実装クラスとして以下の3つのいずれかを使用することを推奨している。
また、本ガイドラインではこれらのクラスを直接使用せず、後述するDelegatingPassowordEncoderを通して使用することを推奨する。
PasswordEncoderの実装クラス

実装クラス

説明

Pbkdf2PasswordEncoder
PBKDF2アルゴリズムを使用してパスワードのハッシュ化及び照合を行う実装クラス。
本ガイドラインでは、このクラスを使用することを推奨している。
詳細は、Pbkdf2PasswordEncoderのJavaDocを参照されたい。
BCryptPasswordEncoder
BCryptアルゴリズムを使用してパスワードのハッシュ化及び照合を行う実装クラス。
詳細は、BCryptPasswordEncoderのJavaDocを参照されたい。
Argon2PasswordEncoder
Argon2アルゴリズムを使用してパスワードのハッシュ化及び照合を行う実装クラス。
詳細は、Argon2PasswordEncoderのJavaDocを参照されたい。
SCryptPasswordEncoder
SCryptアルゴリズムを使用してパスワードのハッシュ化及び照合を行う実装クラス。
詳細は、SCryptPasswordEncoderのJavaDocを参照されたい。

Note

OWASP(Open Web Application Security Project)ではFIPSに準ずるPBKDF2アルゴリズムが推奨されている。

ブランクプロジェクトが提供するPasswordEncoderの定義も、デフォルトでPbkdf2PasswordEncoderを使用する定義となっている。

Note

Argon2PasswordEncoderまたはSCryptPasswordEncoderを使用する場合は、ブランクプロジェクトのデフォルト設定から変更する必要がある。

ApplicationContextConfig.javaのコメントアウトを外し、SCryptPasswordEncoderの定義を有効化する。

  • ApplicationContextConfig.java

    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
        // omitted
        // When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
        // idToPasswordEncoder.put("argon2", argon2PasswordEncoder());
        // idToPasswordEncoder.put("scrypt", sCryptPasswordEncoder());
        return new DelegatingPasswordEncoder("pbkdf2", idToPasswordEncoder);
    }
    
    // omitted
    
    // @Bean
    // public Argon2PasswordEncoder argon2PasswordEncoder() {
    //     return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    // }
    
    //@Bean
    //public SCryptPasswordEncoder sCryptPasswordEncoder() {
    //    return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
    //}
    

依存ライブラリとして不足しているbcprov-jdk18onを追加する。

pom.xmlに以下のdependencyを追加すれば良い。

pom.xml

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
</dependency>

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。


Note

アプリケーションの要件によっては、上記以外の非推奨なPasswordEncoderの実装クラスを利用する必要がある場合もある。

非推奨アルゴリズムのPasswordEncoderの利用」では非推奨の実装クラスの一つであるMessageDigestPasswordEncoderを利用する方法について解説する。


9.2.2.5.1. DelegatingPasswordEncoder

DelegatingPasswordEncoderは、ハッシュ化されたパスワードの照合に複数のPasswordEncoderから適切なものを選択するためのストラテジインタフェースである。これにより、データベース等に格納された様々なアルゴリズムでハッシュ化されたパスワードを、アプリケーションの変更無しに扱うことが可能となる。
なお、DelegatingPasswordEncoderがハッシュ化されたアルゴリズムを判定するには、ハッシュ化されたパスワードの先頭にアルゴリズムを示すキーを含む必要があり、DelegatingPasswordEncoderがパスワードをハッシュ化する際には、このキーが自動的に付与される。
../_images/AuthenticationDelegatingPasswordEncoder.png

DelegatingPasswordEncoderの解説

項番

説明

(1)
User1、User2についてパスワードの照合を行う。データストアにはDelegatingPasswordEncoderでハッシュ化したパスワードが格納されている。
データストアに格納されたパスワードはDelegatingPasswordEncoderがハッシュ化を行っており、User1はBCryptPasswordEncoder、User2はPbkdf2PasswordEncoderが用いられている。
なお、この解説ではデータストアからDaoAuthenticationProviderにユーザ情報を引き渡す際に経由するUserDetailsServiceの実装クラス等を省略しているため注意されたい。
(2)
DelegatingPasswordEncoderを用いて照合を行う。照合の際は、データストアに格納されたハッシュ化されたパスワードからプレフィックスを読み取り適切なPasswordEncoderに処理を委譲する。
User1のハッシュ値はプレフィックスとしてbcryptが付与されているためBCryptPasswordEncoderで照合が行われ、User2のハッシュ値はpbkdf2が付与されているためPbkdf2PasswordEncoderで照合が行われる。

ブランクプロジェクトではPbkdf2PasswordEncoderを使用するDelegatingPasswordEncoderが定義されている。

ここではブランクプロジェクトで定義されているPasswordEncoderをもとに解説を行う。

  • ApplicationContextConfig.javaの定義

    // (1)
    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
        idToPasswordEncoder.put("pbkdf2", pbkdf2PasswordEncoder()); // (4)
        idToPasswordEncoder.put("bcrypt", bCryptPasswordEncoder()); // (4)
        // When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
        // idToPasswordEncoder.put("argon2", argon2PasswordEncoder); // (4)
        // idToPasswordEncoder.put("scrypt", sCryptPasswordEncoder); // (4)
        return new DelegatingPasswordEncoder("pbkdf2", idToPasswordEncoder); // (2)(3)
    }
    
    @Bean
    public Pbkdf2PasswordEncoder pbkdf2PasswordEncoder() {
        return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
    
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // @Bean
    // public Argon2PasswordEncoder argon2PasswordEncoder() {
    //     return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    // }
    
    // @Bean
    // public SCryptPasswordEncoder sCryptPasswordEncoder() {
    //     return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
    // }
    

    項番

    説明

    (1)
    DelegatingPasswordEncoderをid passwordEncoderで定義する。
    (2)
    idToPasswordEncoderで登録したPasswordEncoderの内、ハッシュ化に使用するもののkey値をidForEncodeに指定する。
    (3)
    idToPasswordEncoderPasswordEncoderの実装を格納したMapを指定する。
    (4)
    PasswordEncoderの実装をMapに格納する。
    ハッシュ化したパスワードのプレフィックスがMapkeyと一致すると、そのkeyで格納されたPasswordEncoderを使用して照合が行われる。
    また、前述のidForEncodekeyが一致するPasswordEncoderを使用してハッシュ化が行われる。
    ハッシュ化の際にはkeyに指定した値がプレフィックスとして付与される。

Warning

セキュリティに関わる注意点

実際のアプリケーション開発では、セキュリティ上のリスクを軽減するためidForEncodeidToPasswordEncoderkey値にアルゴリズム名を推測できないような値を指定することを推奨する。

また、PasswordEncoderインタフェースの実装クラスを利用する際はPasswordEncoderのカスタマイズについても参照し、セキュリティ要件を満たすように変更を加えられたい。

Note

Pbkdf2PasswordEncoderArgon2PasswordEncoderSCryptPasswordEncoderはファクトリーメソッドとしてdefaultsForSpringSecurity_v5_8を使用している。

Pbkdf2PasswordEncoderの場合、以下でハッシュ化が実行される。業務要件に応じ適切に変更されたい。

  • アルゴリズム : SHA-256

  • ソルト : 16 バイト

  • 反復回数 : 310,000 回

カスタマイズ方法はPasswordEncoderのカスタマイズを参照されたい。


ハッシュ化を行うクラスでは、PasswordEncoderをDIコンテナからインジェクションして使用する。

@Service
@Transactional
public class AccountServiceImpl implements AccountService {

    @Inject
    AccountRepository accountRepository;

    @Inject
    PasswordEncoder passwordEncoder; // (1)

    public Account register(Account account, String rawPassword) {
        // omitted
        String encodedPassword = passwordEncoder.encode(rawPassword); // (2)
        account.setPassword(encodedPassword);
        // omitted
        return accountRepository.save(account);
    }

}

項番

説明

(1)
PasswordEncoderをインジェクションする。
(2)
インジェクションしたPasswordEncoderのメソッドを呼び出す。
ここでは、データストアに保存するパスワードをハッシュ化している。

Note

Pbkdf2以外のアルゴリズムを使用する場合

パスワードのハッシュ化に使用するPasswordEncoderを変更するには、idForEncodeに使用したいPasswordEncoderkey値(bcryptscrypt@SpringSecurity_v5_8)に指定すれば良い。


Warning

既存のアプリケーションにDelegatingPasswordEncoderを適用する際の注意点

PasswordEncoderに関する注意点

既に運用しているシステムでは、パスワードにプレフィックスは付与されていない。

プレフィックスが付与されていないパスワードをそのまま照合に使用するには、以下で示すようにdefaultPasswordEncoderForMatchesプロパティで照合に使用するエンコーダを指定する必要がある。

  • ApplicationContextConfig.javaの変更

    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
        idToPasswordEncoder.put("pbkdf2", pbkdf2PasswordCustomEncoder);
        DelegatingPasswordEncoder bean = new DelegatingPasswordEncoder("pbkdf2", idToPasswordEncoder);
        bean.setDefaultPasswordEncoderForMatches(passwordEncoderUsedBefore()); // (1)
        return bean;
    }
    

    項番

    説明

    (1)
    defaultPasswordEncoderForMatchesに移行前に使用していたPasswordEncoderを指定する。
    これにより、プレフィックスが付与されていないハッシュ値に対して、指定したPasswordEncoderで照合が行われるようになる。

defaultPasswordEncoderForMatchesプロパティに照合に使用するエンコーダを指定せずに、DelegatingPasswordEncoderを使用してプレフィックスが付与されていないハッシュ値を照合しようとすると、デフォルトで設定されているUnmappedIdPasswordEncoderが使用され、IllegalArgumentExceptionが発生する。


データストアに関する注意点

ハッシュ化されたパスワードを格納するデータベースなどでは、プレフィックスが付与されることを考慮する必要がある。


9.2.2.6. 認証イベントのハンドリング

Spring Securityは、Spring Frameworkが提供しているイベント通知の仕組みを利用して、認証処理の処理結果を他のコンポーネントと連携する仕組みを提供している。

この仕組みを利用すると、以下のようなセキュリティ要件をSpring Securityの認証機能に組み込むことが可能である。

  • 認証成功、失敗などの認証履歴をデータベースやログに保存する。

  • パスワードを連続して間違った場合にアカウントをロックする。

認証イベントの通知は、以下のような仕組みで行われる。

../_images/AuthenticationEventNotification.png

イベント通知の仕組み

項番

説明

(1)
Spring Securityの認証機能は、認証結果(認証情報や認証例外)をAuthenticationEventPublisherに渡して認証イベントの通知依頼を行う。
(2)
AuthenticationEventPublisherインタフェースのデフォルトの実装クラスは認証結果に対応する認証イベントクラスのインスタンスを生成し、ApplicationEventPublisherに渡してイベントの通知依頼を行う。
(3)
ApplicationEventPublisherインタフェースの実装クラスは、ApplicationListenerインタフェースの実装クラスにイベントを通知する。
(4)
ApplicationListenerの実装クラスの一つであるApplicationListenerMethodAdaptorは、@org.springframework.context.event.EventListenerが付与されているメソッドを呼び出してイベントを通知する。
Spring Securityで使用しているイベントは、認証が成功したことを通知するイベントと認証が失敗したことを通知するイベントの2種類に分類される。
以下にSpring Securityが用意しているイベントクラスを説明する。

9.2.2.6.1. 認証成功イベント

認証が成功した時にSpring Securityが通知する主なイベントは以下の3つである。
この3つのイベントは途中でエラーが発生しなければ、以下の順番ですべて通知される。
認証が成功したことを通知するイベントクラス

イベントクラス

説明

AuthenticationSuccessEvent

AuthenticationProviderによる認証処理が成功したことを通知するためのイベントクラス。 このイベントをハンドリングすると、クライアントが正しい認証情報を指定したことを検知することが可能である。 なお、このイベントをハンドリングした後の後続処理でエラーが発生する可能性がある点に注意されたい。

SessionFixationProtectionEvent

セッション固定攻撃対策の処理(セッションIDの変更処理)が成功したことを通知するためのイベントクラス。 このイベントをハンドリングすると、変更後のセッションIDを検知することが可能になる。

InteractiveAuthenticationSuccessEvent

認証処理がすべて成功したことを通知するためのイベントクラス。 このイベントをハンドリングすると、画面遷移を除くすべての認証処理が成功したことを検知することが可能になる。


9.2.2.6.2. 認証失敗イベント

認証が失敗した時にSpring Securityが通知する主なイベントは以下の通り。 認証に失敗した場合は、いずれか一つのイベントが通知される。

認証が失敗したことを通知するイベントクラス

イベントクラス

説明

AuthenticationFailureBadCredentialsEvent
BadCredentialsExceptionが発生したことを通知するためのイベントクラス。
AuthenticationFailureDisabledEvent
DisabledExceptionが発生したことを通知するためのイベントクラス。
AuthenticationFailureLockedEvent
LockedExceptionが発生したことを通知するためのイベントクラス。
AuthenticationFailureExpiredEvent
AccountExpiredExceptionが発生したことを通知するためのイベントクラス。
AuthenticationFailureCredentialsExpiredEvent
CredentialsExpiredExceptionが発生したことを通知するためのイベントクラス。
AuthenticationFailureServiceExceptionEvent
AuthenticationServiceExceptionが発生したことを通知するためのイベントクラス。

9.2.2.6.3. イベントリスナの作成

認証イベントの通知を受け取って処理を行いたい場合は、@EventListenerを付与したメソッドを実装したクラスを作成し、DIコンテナに登録する。

  • イベントリスナクラスの実装例

package com.examples.domain.common.event; // (1)

@Component // (1)
public class AuthenticationEventListeners {

    private static final Logger logger =
            LoggerFactory.getLogger(AuthenticationEventListeners.class);

    @EventListener(AuthenticationFailureBadCredentialsEvent.class) // (2)
    public void handleBadCredentials(
        AuthenticationFailureBadCredentialsEvent event) { // (3)
        logger.info("Bad credentials is detected. username : {}", event.getAuthentication().getName());
        // omitted
    }

項番

説明

(1)
コンポーネントスキャン機能を利用してイベントリスナクラスを登録するため、@Componentをクラスに付与する。

Warning

イベントリスナクラスの配置について

Spring Securityが参照するWebアプリケーション用のアプリケーションコンテキストに登録するため、アプリケーションのdomainパッケージ配下に置くこと。

ただし、SessionFixationProtectionEventはspring-security-webに定義されているため、ブランクプロジェクトのデフォルト設定ではdomainモジュールから参照することができない。

このイベントをハンドリングする場合はwebモジュール(webパッケージ配下)に置くことになるが、スキャン対象の定義が煩雑になるためコンポーネントスキャン機能を利用せずbean定義することを検討されたい。

(2)
@EventListenerをメソッドに付与したメソッドを作成する。
イベントリスナは属性値に指定された認証イベントクラスを処理する。認証イベントクラスは複数指定することができる。
(3)
メソッドの引数にハンドリングしたい認証イベントクラスを指定する。

上記例では、クライアントが指定した認証情報に誤りがあった場合に通知されるAuthenticationFailureBadCredentialsEventをハンドリングするクラスを作成する例としているが、他のイベントも同じ要領でハンドリングすることが可能である。

Tip

総当たり攻撃による不正ログインの兆候を検出するための方法として、ログイン認証時のログを監視することがあげられる。

実装例のようなAuthenticationFailureBadCredentialsEventをハンドリングするイベントリスナを作成して認証情報の誤りをログ情報として出力することで、Spring Securityを使用した認証時のログを監視することが可能になる。


9.2.2.7. ログアウト

Spring Securityは、以下のような流れでログアウト処理を行う。

../_images/AuthenticationLogout.png

ログアウト処理の仕組み

項番

説明

(1)
クライアントは、ログアウト処理を行うためのパスにリクエストを送信する。
(2)
LogoutFilterは、LogoutHandlerのメソッドを呼び出し、実際のログアウト処理を行う。
(3)
LogoutFilterは、LogoutSuccessHandlerのメソッドを呼び出し、画面遷移を行う。

LogoutHandlerの実装クラスは複数存在し、それぞれ以下の役割をもっている。

主なLogoutHandlerの実装クラス

実装クラス

説明

SecurityContextLogoutHandler
ログインユーザーの認証情報のクリアとセッションの破棄を行うクラス。
CookieClearingLogoutHandler
指定したクッキーを削除するためのレスポンスを行うクラス。
CsrfLogoutHandler
CSRF対策用トークンの破棄を行うクラス。
LogoutSuccessEventPublishingLogoutHandler
LogoutSuccessEventクラスのインスタンスを生成し、ApplicationEventPublisherに渡してイベントの通知依頼を行うクラス。
これらのLogoutHandlerは、Spring Securityが提供しているbean定義をサポートするクラスが自動でLogoutFilterに設定する仕組みになっているため、基本的にはアプリケーションの開発者が直接意識する必要はない。
また、Remember Me認証機能を有効にすると、Remember Me認証用のTokenを破棄するためのLogoutHandlerの実装クラスも設定される。

Note

Clear-Site-Dataヘッダの付与

Spring Securityでは、Webサイトの閲覧用データ(クッキー、ストレージ、キャッシュ)を削除するためのClear-Site-Dataヘッダを付与するorg.springframework.security.web.header.writers.ClearSiteDataHeaderWriterを提供している。

本機能はLogoutHandlerの仕組みを用いて適用されるが、自動的には適用されない。

適用するにはLogoutFilterをbean定義し、org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandlerを用いて登録する必要がある。


9.2.2.7.1. ログアウト処理の適用

ログアウト処理を適用するためには、以下のようなbean定義を行う。

  • SpringSecurityConfig.javaの定義例

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // omitted
        http.logout(Customizer.withDefaults()); // (1)
        // omitted
        return http.build();
    }
    

    項番

    説明

    (1)
    HttpSecurity#logoutを呼び出すことで、ログアウト処理が有効となる。

9.2.2.7.2. デフォルトの動作

Spring Securityのデフォルトの動作では、/logoutというパスにリクエストを送るとログアウト処理が行われる。
ログアウト処理では、「ログインユーザーの認証情報のクリア」「セッションの破棄」が行われる。

また、

  • CSRF対策を行っている場合は、「CSRF対策用トークンの破棄」

  • Remember Me認証機能を使用している場合は、「Remember Me認証用のTokenの破棄」

も行われる

  • ログアウト処理を呼び出すためのJSPの実装例

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%-- omitted --%>
<form:form action="${pageContext.request.contextPath}/logout" method="post"> <%-- (1) --%>
    <button>ログアウト</button>
</form:form>

項番

説明

(1)
ログアウト用のフォームを作成する。
また、<form:form>を使用することで、CSRF対策用のトークン値がリクエストパラメータで送信される。
CSRF対策については、「CSRF対策」で説明する。

Note

CSRFトークンの送信

CSRF対策を有効にしている場合は、CSRF対策用のトークンをPOSTメソッドで送信する必要がある。


9.2.2.8. ログアウト成功時のレスポンス

Spring Securityは、ログアウト成功時のレスポンスを制御するためのコンポーネントとして、LogoutSuccessHandlerというインタフェースと実装クラスを提供している。

主なLogoutSuccessHandlerの実装クラス

実装クラス

説明

SimpleUrlLogoutSuccessHandler
指定したパス(defaultTargetUrl)にリダイレクトを行う実装クラス。
HttpStatusReturningLogoutSuccessHandler
ログアウト成功時のレスポンスに任意のステータスコードを設定する実装クラス。
デフォルトでは200(OK)が設定される。
ログアウト成功時にリダイレクトを行うのが望ましくないRESTful Web Serviceのようなアプリケーションで有用である。
DelegatingLogoutSuccessHandler
RequestMatcherインタフェースの仕組みを利用して、指定されたリクエストのパターンに対応するLogoutSuccessHandlerインタフェースの実装クラスに処理を委譲する実装クラス。

9.2.2.8.1. デフォルトの動作

Spring Securityのデフォルトの動作では、ログインフォームを表示するためのパスにlogoutというクエリパラメータが付与されたURLにリダイレクトする。

例として、ログインフォームを表示するためのパスが/loginの場合は/login?logoutにリダイレクトされる。


9.2.2.9. ログアウト成功時の認証イベントのハンドリング

Spring Securityでは、認証イベントのハンドリングと同様に、ログアウト処理の処理結果を他のコンポーネントと連携する仕組みを提供している。

ログアウト成功時の認証イベントの通知は、以下のような仕組みで行われる。

../_images/AuthenticationLogoutEvent.png

イベント通知の仕組み

項番

説明

(1)
ログアウト処理が成功した後、LogoutSuccessEventPublishingLogoutHandlerは認証イベントクラスのインスタンスを生成し、ApplicationEventPublisherに渡してイベントの通知依頼を行う。

以下にSpring Securityが用意しているイベントクラスを説明する。


9.2.2.9.1. ログアウト成功イベント

ログアウトが成功した時にSpring Securityが通知するイベントは以下の1つである。

ログアウトが成功したことを通知するイベントクラス

イベントクラス

説明

LogoutSuccessEvent

ログアウトが成功したことを通知するためのイベントクラス。
このイベントをハンドリングすると、クライアントがログアウトし、認証情報が破棄されたことを検知することが可能である。
なお、このイベントをハンドリングした後の後続処理でエラーが発生する可能性がある点に注意されたい。

ログアウト成功イベントの通知を受け取って処理を行う方法については、イベントリスナの作成を参照されたい。


9.2.2.10. 認証情報へのアクセス

認証されたユーザーの認証情報は、Spring Securityのデフォルト実装ではセッションに格納される。
セッションに格納された認証情報は、リクエスト毎にSecurityContextHolderFilterクラスによってSecurityContextHolderというクラスに格納され、同一スレッド内であればどこからでもアクセスすることができるようになる。

ここでは、認証情報からUserDetailsを取得し、取得したUserDetailsが保持している情報にアクセスする方法を説明する。


9.2.2.10.1. Javaからのアクセス

一般的な業務アプリケーションでは、「いつ」「誰が」「どのデータに」「どのようなアクセスをしたか」を記録する監査ログを取得することがある。
このような要件を実現する際の「誰が」は、認証情報から取得することができる。
  • Javaから認証情報へアクセスする実装例

Authentication authentication =
        SecurityContextHolder.getContext().getAuthentication(); // (1)
String userUuid = null;
if (authentication.getPrincipal() instanceof AccountUserDetails) {
    AccountUserDetails userDetails =
            AccountUserDetails.class.cast(authentication.getPrincipal()); // (2)
    userUuid = userDetails.getAccount().getUserUuid(); // (3)
}
if (logger.isInfoEnabled()) {
    logger.info("type:Audit\ tuserUuid:{}\ tresource:{}\ tmethod:{}",
            userUuid, httpRequest.getRequestURI(), httpRequest.getMethod());
}

項番

説明

(1)
SecurityContextHolderから認証情報(Authenticationオブジェクト) を取得する。
(2)
Authentication#getPrincipal()メソッドを呼び出して、UserDetailsオブジェクトを取得する。
認証済みでない場合(匿名ユーザーの場合)は、匿名ユーザーであることを示す文字列が返却されるため注意されたい。
(3)
UserDetailsから処理に必要な情報を取得する。
ここでは、ユーザーを一意に識別するための値(UUID)を取得している。

Warning

認証情報へのアクセスと結合度

Spring Securityのデフォルト実装では、認証情報をスレッドローカルの変数に格納しているため、リクエストを受けたスレッドと同じスレッドであればどこからでもアクセス可能である。

この仕組みは便利ではあるが、認証情報を必要とするクラスがSecurityContextHolderクラスに直接依存してしまうため、乱用するとコンポーネントの疎結合性が低下するので注意が必要である。

Spring Securityでは、Spring MVCの機能と連携してコンポーネント間の疎結合性を保つための仕組みを別途提供している。

Spring MVCとの連携方法については、「認証処理とSpring MVCの連携」で説明する。

本ガイドラインではSpring MVCとの連携を使用して認証情報を取得することを推奨する。

Note

認証処理用のフィルタ(FORM_LOGIN_FILTER)をカスタマイズする場合は、<sec:concurrency-control>要素の指定に加えて、以下の2つのSessionAuthenticationStrategyクラスを有効化する必要がある。

  • org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy
    認証成功後にログインユーザ毎のセッション数をチェックするクラス。
  • org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
    認証に成功したセッションをセッション管理領域に登録するクラス。

9.2.2.10.2. JSPからのアクセス

一般的なWebアプリケーションでは、ログインユーザーのユーザー情報などを画面に表示することがある。
このような要件を実現する際のログインユーザーのユーザー情報は、認証情報から取得することができる。
JSPでの認証情報へのアクセスの方法について説明する。
  • JSPから認証情報へアクセスする実装例

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%-- omitted --%>
ようこそ、
<sec:authentication property="principal.account.lastName"/> <%-- (1) --%>
さん。

項番

説明

(1)
Spring Securityから提供されている<sec:authentication>タグを使用して、認証情報(Authenticationオブジェクト) を取得する。
property属性にアクセスしたいプロパティへのパスを指定する。
ネストしているオブジェクトへアクセスしたい場合は、プロパティ名を”.“ でつなげればよい。

Tip

認証情報の表示方法

ここでは、認証情報が保持するユーザー情報を表示する際の実装例を説明したが、var属性とscope属性を組み合わせて任意のスコープ変数に値を格納することも可能である。

ログインユーザーの状態によって表示内容を切り替えたい場合は、ユーザー情報を変数に格納しておき、JSTLのタグライブラリなどを使って表示を切り替えることが可能である。

上記の例は、以下のように記述することでも実現することができる。

本例では、scope属性を省略しているため、pageスコープが適用される。

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%-- omitted --%>
<sec:authentication var="principal" property="principal"/>
<%-- omitted --%>
ようこそ、
${f:h(principal.account.lastName)}
さん。

9.2.2.10.3. Thymeleafからのアクセス

JSPと同様にThymeleafでもログインユーザーのユーザー情報は、認証情報から取得することができる。
  • Thymeleafのテンプレートで認証情報へアクセスする実装例

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!--/* omitted */-->
ようこそ、
<span sec:authentication="principal.account.lastName"></span> <!--/* (1) */-->
さん。

項番

説明

(1)
属性値にSpring Security Dialectから提供されているsec:authentication属性を使用して、認証情報(Authenticationオブジェクト) を取得する。
アクセスしたいプロパティへのパスを指定する。
ネストしているオブジェクトへアクセスしたい場合は、プロパティ名を”.“ でつなげればよい。

Tip

#authenticationの紹介

ここでは、sec:authentication属性を用いて認証情報が保持するユーザー情報を表示する際の実装例を説明したが、Spring Security Dialectから提供されている#authenticationを用いても、ThymeleafのテンプレートHTMLから認証情報にアクセスする事が可能である。

#authenticationは、 変数式${}式にて使用できるため、条件判定やリテラル置換等sec:authentication属性より複雑な使い方が可能である。

上記の例は、以下のように記述できる

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"><!--/* (1) */-->
<!--/* omitted */-->
<p th:text="|ようこそ、${#authentication.principal.account.lastName}さん。|"></p><!--/* (2) */-->

項番

説明

(1)
sec:authentication属性を使用する際には<html>タグにxmlns:sec属性を定義していたが、
#authenticationを使用する際には、xmlns:sec属性の定義は不要である。
(2)
#authenticationにて認証情報よりlastNameを取得し、lastNameの前後にリテラル置換を行っている。

9.2.2.11. 認証処理とSpring MVCの連携

Spring Securityは、Spring MVCと連携するためのコンポーネントをいくつか提供している。
ここでは、認証処理と連携するためのコンポーネントの使い方を説明する。

9.2.2.11.1. 認証情報へのアクセス

Spring Securityは、認証情報(UserDetails)をSpring MVCのコントローラーのメソッドに引き渡すためのコンポーネントとして、AuthenticationPrincipalArgumentResolverクラスを提供している。
AuthenticationPrincipalArgumentResolverを使用すると、コントローラーのメソッド引数としてUserDetailsインタフェースまたはその実装クラスのインスタンスを受け取ることができるため、コンポーネントの疎結合性を高めることができる。
認証情報(UserDetails)をコントローラーの引数として受け取るためには、まずAuthenticationPrincipalArgumentResolverをSpring MVCに適用する必要がある。
AuthenticationPrincipalArgumentResolverを適用するためのbean定義は以下の通りである。
なお、ブランクプロジェクトにはAuthenticationPrincipalArgumentResolverが設定済みである。
  • SpringMvcConfig.javaの定義例

@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        // omitted
        argumentResolvers.add(authenticationPrincipalArgumentResolver()); // (1)
        // omitted
    }

    // (1)
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver();
    }

項番

説明

(1)
HandlerMethodArgumentResolverの実装クラスとして、AuthenticationPrincipalArgumentResolverをSpring MVCに適用する。

認証情報(UserDetails)をコントローラーのメソッドで受け取る際は、以下のようなメソッドを作成する。

  • 認証情報(UserDetails)を受け取るメソッドの作成例

@RequestMapping("account")
@Controller
public class AccountController {

    public String view(
            @AuthenticationPrincipal AccountUserDetails userDetails, // (1)
            Model model) {
        model.addAttribute(userDetails.getAccount());
        return "profile";
    }

}

項番

説明

(1)
認証情報(UserDetails) を受け取るための引数を宣言し、@org.springframework.security.core.annotation.AuthenticationPrincipalを引数アノテーションとして指定する。
AuthenticationPrincipalArgumentResolverは、@AuthenticationPrincipalが付与されている引数に認証情報(UserDetails)が設定される。

9.2.3. How to extend

本節では、Spring Securityが用意しているカスタマイズポイントや拡張方法について説明する。

Spring Securityは、多くのカスタマイズポイントを提供しているため、すべてのカスタマイズポイントを紹介することはできないため、ここでは代表的なカスタマイズポイントに絞って説明を行う。


9.2.3.1. フォーム認証のカスタマイズ

フォーム認証処理のカスタマイズポイントを説明する。


9.2.3.1.1. 認証パスの変更

Spring Securityのデフォルトでは、認証処理を実行するためのパスは「/login」であるが、以下のようなbean定義を行うことで変更することが可能である。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChainJsp0102(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.loginProcessingUrl("/authentication")); // (1)
    // omitted
    return http.build();
}

項番

説明

(1)
loginProcessingUrlに認証処理を行うためのパスを指定する。

Note

認証処理のパスを変更した場合は、ログインフォームのリクエスト先も変更する必要がある。


9.2.3.1.2. 資格情報を送るリクエストパラメータ名の変更

Spring Securityのデフォルトでは、資格情報(ユーザー名とパスワード)を送るためのリクエストパラメータは「username」と「password」であるが、以下のようなbean定義を行うことで変更することが可能である。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.usernameParameter("uid") // (1)
            .passwordParameter("pwd")); // (2)
    // omitted
    return http.build();
}

項番

説明

(1)
usernameParameterにユーザー名のリクエストパラメータ名を指定する。
(2)
passwordParameterにパスワードのリクエストパラメータ名を指定する。

Note

リクエストパラメータ名を変更した場合は、ログインフォーム内の項目名も変更する必要がある。


9.2.3.2. 認証成功時のレスポンスのカスタマイズ

認証成功時のレスポンスのカスタマイズポイントを説明する。


9.2.3.2.1. デフォルト遷移先の変更

ログインフォームを自分で表示して認証処理を行った後の遷移先(デフォルトURL)は、Webアプリケーションのルートパス(”/“ )だが、以下のようなbean定義を行うことで変更することが可能である。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.defaultSuccessUrl("/menu")); // (1)
    // omitted
    return http.build();
}

項番

説明

(1)
defaultSuccessUrlに認証成功時に遷移するデフォルトのパスを指定する。

9.2.3.2.2. 遷移先の固定化

Spring Securityのデフォルトの動作では、未認証時に認証が必要なページへのリクエストを受信した場合は、受信したリクエストを一旦HTTPセッションに保存し、認証ページに遷移する。
認証成功時にリクエストを復元してリダイレクトするが、以下のようなbean定義を行うことで常に同じ画面に遷移させることが可能である。
  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.defaultSuccessUrl("/menu"), true); // (1)
    // omitted
    return http.build();
}

項番

説明

(1)
defaultSuccessUrlの第二引数にtrueを指定する。 (第二引数を指定しない場合はfalseとなる。)

9.2.3.2.3. AuthenticationSuccessHandlerの適用

Spring Securityが提供しているデフォルトの動作をカスタマイズする仕組みだけでは要件をみたせない場合は、以下のようなbean定義を行うことでAuthenticationSuccessHandlerインタフェースの実装クラスを直接適用することができる。

  • SpringSecurityConfig.javaの定義例

// (1)
@Bean("authenticationSuccessHandler")
public MyAuthenticationSuccessHandler authenticationSuccessHandler() {
    return new MyAuthenticationSuccessHandler();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.successHandler(authenticationSuccessHandler())); // (2)
    // omitted
    return http.build();
}

項番

説明

(1)
AuthenticationSuccessHandlerインタフェースの実装クラスをbean定義する。
(2)
authentication-success-handler-ref属性に定義したauthenticationSuccessHandlerを指定する。

Warning

AuthenticationSuccessHandlerの責務

AuthenticationSuccessHandlerは、認証成功時におけるWeb層の処理(主に画面遷移に関する処理)を行うためのインタフェースである。

そのため、認証失敗回数のクリアなどのビジネスルールに依存する処理(ビジネスロジック)をこのインタフェースの実装クラスを経由して呼び出すべきではない。

ビジネスルールに依存する処理の呼び出しは、前節で紹介している「認証イベントのハンドリング」の仕組みを使用されたい。


9.2.3.3. 認証失敗時のレスポンスのカスタマイズ

認証失敗時のレスポンスのカスタマイズポイントを説明する。


9.2.3.3.1. 遷移先の変更

Spring Securityのデフォルトの動作では、ログインフォームを表示するためのパスにerrorというクエリパラメータが付与されたURLにリダイレクトするが、以下のようなbean定義を行うことで変更することが可能である。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.failureUrl("/loginFailure")); // (1)
    // omitted
       return http.build();
}

項番

説明

(1)
failureUrlに認証失敗時に遷移するパスを指定する。

9.2.3.3.2. AuthenticationFailureHandlerの適用

Spring Securityが提供しているデフォルトの動作をカスタマイズする仕組みだけでは要件をみたせない場合は、以下のようなbean定義を行うことでAuthenticationFailureHandlerインタフェースの実装クラスを直接適用することができる。

  • SpringSecurityConfig.javaの定義例

// (1)
@Bean("authenticationFailureHandler")
public ExceptionMappingAuthenticationFailureHandler authenticationFailureHandler() {
    ExceptionMappingAuthenticationFailureHandler bean = new ExceptionMappingAuthenticationFailureHandler();
    bean.setDefaultFailureUrl("/login/systemError"); // (2)
    Properties mappings = new Properties();
    mappings.put("org.springframework.security.authentication.BadCredentialsException", "/login/badCredentials"); // (4)
    mappings.put("org.springframework.security.core.userdetails.UsernameNotFoundException", "/login/usernameNotFound"); // (5)
    mappings.put("org.springframework.security.authentication.DisabledException", "/login/disabled"); // (6)
    // omitted
    bean.setExceptionMappings(mappings); // (3)
    return bean;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http.formLogin(login -> login.failureHandler(jspAuthenticationFailureHandler)); // (7)
    // omitted
    return http.build();
}
項番
説明
(1)
AuthenticationFailureHandlerインタフェースの実装クラスをbean定義する。
(2)
defaultFailureUrl属性にデフォルトの遷移先のURLを指定する。
下記(4)-(6)の定義に合致しない例外が発生した際は、本設定の遷移先に遷移する。
(3)
exceptionMappingsプロパティにハンドルするorg.springframework.security.authentication.AuthenticationServiceExceptionの実装クラスと例外発生時の遷移先を Map形式で設定する。
キーにorg.springframework.security.authentication.AuthenticationServiceException実装クラスを設定し、値に遷移先URLを設定する。
(4)
BadCredentialsException
パスワード照合失敗による認証エラー時にスローされる。
(5)
UsernameNotFoundException
不正ユーザーID(存在しないユーザーID)による認証エラー時にスローされる。
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
継承したクラスを認証プロバイダに指定している場合、hideUserNotFoundExceptionsプロパティをfalseに変更しないと本例外は、BadCredentialsExceptionに変更される。
(6)
DisabledException
無効ユーザーIDによる認証エラー時にスローされる。
(7)
failureHandlerauthenticationFailureHandlerを設定する。

Note

例外発生時の制御

exceptionMappingsプロパティに定義した例外が発生した場合、例外にマッピングした遷移先にリダイレクトされるが、発生した例外オブジェクトがセッションスコープに格納されないため、Spring Securityが生成したエラーメッセージを画面に表示する事ができない。

そのため、遷移先の画面で表示するエラーメッセージは、リダイレクト先の処理(Controller又はViewの処理)で生成する必要がある。

また、以下のプロパティを参照する処理が呼び出されないため、設定値を変更しても動作が変わらないという点を補足しておく。

  • useForward

  • allowSessionCreation


9.2.3.4. ログアウト処理のカスタマイズ

ログアウト処理のカスタマイズポイントを説明する。


9.2.3.4.1. ログアウトパスの変更

Spring Securityのデフォルトでは、ログアウト処理を実行するためのパスは「/logout」であるが、以下のようなbean定義を行うことで変更することが可能である。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // omitted
    http.logout(logout -> logout.logoutUrl("/auth/logout")); // (1)
    // omitted
    return http.build();
}

項番

説明

(1)
logoutUrlを設定し、ログアウト処理を行うパスを指定する。

Note

ログアウトパスを変更した場合は、ログアウトフォームのリクエスト先も変更する必要がある。

Tip

システムエラー発生時の振る舞い

システムエラーが発生した場合は、業務継続不可となるケースが多いと考えられる。

システムエラー発生後、業務を継続させたくない場合は、以下のような対策を講じることを推奨する。

  • システムエラー発生時にセッション情報をクリアする。

  • システムエラー発生時に認証情報をクリアする。

ここでは、共通ライブラリの例外ハンドリング機能を使用してシステム例外発生時に認証情報をクリアする例を説明する。

例外ハンドリング機能の詳細については「例外ハンドリング」を参照されたい。

// (1)
public class LogoutSystemExceptionResolver extends SystemExceptionResolver {
    // (2)
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request,
            HttpServletResponse response, java.lang.Object handler,
            java.lang.Exception ex) {

        // SystemExceptionResolverの処理を行う
        ModelAndView resulut = super.doResolveException(request, response,
                handler, ex);

        // 認証情報をクリアする (2)
        SecurityContextHolder.clearContext();

        return resulut;
    }
}

項番

説明

(1)
org.terasoluna.gfw.web.exception.SystemExceptionResolver.SystemExceptionResolverを拡張する。
(2)
認証情報をクリアする。

なお、認証情報をクリアする方法以外にも、セッションをクリアすることでも、同様の要件を満たすことができる。

プロジェクトの要件に合わせて実装されたい。


9.2.3.5. ログアウト成功時のレスポンスのカスタマイズ

ログアウト処理成功時のレスポンスのカスタマイズポイントを説明する。


9.2.3.5.1. 遷移先の変更

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // omitted
    http.logout(logout -> logout.logoutSuccessUrl("/logoutSuccess")); // (1)
    // omitted
    return http.build();
}

項番

説明

(1)
logout-success-url属性を設定し、ログアウト成功時に遷移するパスを指定する。

9.2.3.5.2. LogoutSuccessHandlerの適用

  • SpringSecurityConfig.javaの定義例

// (1)
@Bean("logoutSuccessHandler")
public MyLogoutSuccessHandler logoutSuccessHandler() {
    return new MyLogoutSuccessHandler();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // omitted
    http.logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler())); // (2)
    // omitted
    return http.build();
}

項番

説明

(1)
LogoutSuccessHandlerインタフェースの実装クラスをbean定義する。
(2)
success-handler-ref属性にLogoutSuccessHandlerを設定する。

9.2.3.6. エラーメッセージのカスタマイズ

認証に失敗した場合、Spring Securityが用意しているエラーメッセージが表示されるが、このエラーメッセージは変更することが可能である。

メッセージ変更方法の詳細については、メッセージ管理を参照されたい。


9.2.3.6.1. システムエラー時のメッセージ

認証処理の中で予期しないエラー(システムエラーなど)が発生した場合、InternalAuthenticationServiceExceptionという例外が発生する。
InternalAuthenticationServiceExceptionが保持するメッセージには、原因例外のメッセージが設定されるため、画面にそのまま表示するのは適切ではない。
例えばユーザー情報をデーターベースから取得する時にDBアクセスエラーが発生した場合、SQLExceptionが保持する例外メッセージが画面に表示されることになる。
システムエラーの例外メッセージを画面に表示させないためには、ExceptionMappingAuthenticationFailureHandlerを使用してInternalAuthenticationServiceExceptionをハンドリングし、システムエラーが発生したことを通知するためのパスに遷移させるなどの対応が必要となる。
  • SpringSecurityConfig.javaの定義例

@Bean("authenticationFailureHandler")
public ExceptionMappingAuthenticationFailureHandler authenticationFailureHandler() {
    ExceptionMappingAuthenticationFailureHandler bean = new ExceptionMappingAuthenticationFailureHandler();
    bean.setDefaultFailureUrl("/login?error");
    Properties mappings = new Properties();
    mappings.put("org.springframework.security.authentication.InternalAuthenticationServiceException", "/login?systemError");
    // omitted
    bean.setExceptionMappings(mappings);
    return bean;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.formLogin(login -> login.failureHandler(jspAuthenticationFailureHandler));
    // omitted
    return http.build();
}

ここでは、システムエラーが発生したことを識別するためのクエリパラメータ(systemError)を付けてログインフォームに遷移させている。
遷移先に指定したログインフォームでは、クエリパラメータにsystemErrorが指定されている場合は、認証例外のメッセージを表示するのではなく、固定のエラーメッセージを表示するようにしている。
  • ログインフォームの実装例

<c:choose>
    <c:when test="${param.containsKey('error')}">
        <span style="color: red;">
            <c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}"/>
        </span>
    </c:when>
    <c:when test="${param.containsKey('systemError')}">
        <span style="color: red;">
            System Error occurred.
        </span>
    </c:when>
</c:choose>

Note

ここでは、ログインフォームに遷移させる場合の実装例を紹介したが、システムエラー画面に遷移させてもよい。


9.2.3.7. 認証時の入力チェック

DBサーバへの負荷軽減等で、認証ページにおける、あきらかな入力誤りに対しては、事前にチェックを行いたい場合がある。
このような場合は、Bean Validationを使用した入力チェックも可能である。

9.2.3.7.1. Bean Validationによる入力チェック

以下にBean Validationを使用した入力チェックの例を説明する。
Bean Validationに関する詳細は 入力チェックを参照すること。
  • フォームクラスの実装例

public class LoginForm implements Serializable {

    // omitted
    @NotEmpty // (1)
    private String username;

    @NotEmpty // (1)
    private String password;
    // omitted

}

項番

説明

(1)
本例では、usernamepasswordをそれぞれ必須入力としている。
  • コントローラクラスの実装例

@ModelAttribute
public LoginForm setupForm() { // (1)
    return new LoginForm();
}

@RequestMapping(value = "login")
public String login(@Validated LoginForm form, BindingResult result) {
    // omitted
    if (result.hasErrors()) {
        // omitted
    }
    return "forward:/authenticate"; // (2)
}

項番

説明

(1)
LoginFormを初期化する。
(2)
forwardで<sec:form-login>要素のlogin-processing-url属性に指定したパスにForwardする。
認証に関する設定は、フォーム認証のカスタマイズを参照すること。

加えて、Forwardによる遷移でもSpring Securityの処理が行われるよう、認証パスをSpring Securityサーブレットフィルタに追加する。

  • web.xmlの設定例

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- (1) -->
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/authenticate</url-pattern>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

項番

説明

(1)
Forwardで認証するためのパターンを指定する
ここでは認証パスである/authenticateを指定している。

9.2.3.8. 認証処理の拡張

Spring Securityから提供されている認証プロバイダで対応できない認証要件がある場合は、org.springframework.security.authentication.AuthenticationProviderインタフェースを実装したクラスを作成する必要がある。

ここでは、ユーザー名、パスワード、会社識別子(独自の認証パラメータ)の3つのパラメータを使用してDB認証を行うための拡張例を示す。

Authentication_HowToExtends_LoginForm

上記の要件を実現するためには、以下に示すクラスを作成する必要がある。

項番

説明

(1)
ユーザー名、パスワード、会社識別子を保持するorg.springframework.security.core.Authenticationインタフェースの実装クラス。
ここでは、org.springframework.security.authentication.UsernamePasswordAuthenticationTokenクラスを継承して作成する。
(2)
ユーザー名、パスワード、会社識別子を使用してDB認証を行うorg.springframework.security.authentication.AuthenticationProviderの実装クラス。
ここでは、org.springframework.security.authentication.dao.DaoAuthenticationProviderクラスを継承して作成する。
(3)
ユーザー名、パスワード、会社識別子をリクエストパラメータから取得して、AuthenticationManager(AuthenticationProvider)に渡すAuthenticationを生成するためのAuthentication Filterクラス。
ここでは、org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilterクラスを継承して作成する。

Note

ここでは、認証用のパラメータとして独自のパラメータを追加する例にしているため、Authenticationインタフェースの実装クラスとAuthenticationを生成するためのAuthentication Filterクラスの拡張が必要となる。

ユーザー名とパスワードのみで認証する場合は、AuthenticationProviderインタフェースの実装クラスを作成するだけで、認証処理を拡張することができる。


9.2.3.8.1. Authenticationインタフェースの実装クラスの作成

UsernamePasswordAuthenticationTokenクラスを継承し、ユーザー名とパスワードに加えて、会社識別子(独自の認証パラメータ)を保持するクラスを作成する。

// import omitted
public class CompanyIdUsernamePasswordAuthenticationToken extends
    UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // (1)
    private final String companyId;

    // (2)
    public CompanyIdUsernamePasswordAuthenticationToken(
            Object principal, Object credentials, String companyId) {
        super(principal, credentials);
        this.companyId = companyId;
    }

    // (3)
    public CompanyIdUsernamePasswordAuthenticationToken(
            Object principal, Object credentials, String companyId,
            Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
        this.companyId = companyId;
    }

    public String getCompanyId() {
        return companyId;
    }

}

項番

説明

(1)
会社識別子を保持するフィールドを作成する。
(2)
認証前の情報(リクエストパラメータで指定された情報)を保持するインスタンスを作成する際に使用するコンストラクタを作成する。
(3)
認証済みの情報を保持するインスタンスを作成する際に使用するコンストラクタを作成する。
親クラスのコンストラクタの引数に認可情報を渡すことで、認証済みの状態となる。

9.2.3.8.2. AuthenticationProviderインタフェースの実装クラスの作成

DaoAuthenticationProviderクラスを継承し、ユーザー名、パスワード、会社識別子を使用してDB認証を行うクラスを作成する。

// import omitted
public class CompanyIdUsernamePasswordAuthenticationProvider extends
    DaoAuthenticationProvider {

    // omitted

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {

        // (1)
        super.additionalAuthenticationChecks(userDetails, authentication);

        // (2)
        CompanyIdUsernamePasswordAuthenticationToken companyIdUsernamePasswordAuthentication =
                (CompanyIdUsernamePasswordAuthenticationToken) authentication;
        String requestedCompanyId = companyIdUsernamePasswordAuthentication.getCompanyId();
        String companyId = ((SampleUserDetails) userDetails).getAccount().getCompanyId();
        if (!companyId.equals(requestedCompanyId)) {
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

    @Override
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        String companyId = ((SampleUserDetails) user).getAccount()
                .getCompanyId();
        // (3)
        return new CompanyIdUsernamePasswordAuthenticationToken(user,
                authentication.getCredentials(), companyId,
                user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // (4)
        return CompanyIdUsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication);
    }

}

項番

説明

(1)
親クラスのメソッドを呼び出し、Spring Securityが提供しているチェック処理を実行する。
この処理にはパスワード認証処理も含まれる。
(2)
パスワード認証が成功した場合は、会社識別子(独自の認証パラメータ)の妥当性をチェックする。
上記例では、リクエストされた会社識別子とテーブルに保持している会社識別子が一致するかをチェックしている。
(3)
パスワード認証及び独自の認証処理が成功した場合は、認証済み状態のCompanyIdUsernamePasswordAuthenticationTokenを作成して返却する。
(4)
CompanyIdUsernamePasswordAuthenticationTokenにキャスト可能なAuthenticationが指定された場合に、本クラスを使用して認証処理を行うようにする。

Note

ユーザーの存在チェック、ユーザーの状態チェック(無効ユーザー、ロック中ユーザー、利用期限切れユーザーなどのチェック)は、additionalAuthenticationChecksメソッドが呼び出される前に親クラスの処理として行われる。


9.2.3.8.3. Authentication Filterの作成

UsernamePasswordAuthenticationFilterクラスを継承し、認証情報(ユーザー名、パスワード、会社識別子)をAuthenticationProviderに引き渡すためのAuthentication Filterクラスを作成する。

attemptAuthenticationメソッドの実装は、UsernamePasswordAuthenticationFilterクラスのメソッドをコピーしてカスタマイズしたものである。

// import omitted
public class CompanyIdUsernamePasswordAuthenticationFilter extends
    UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: "
                    + request.getMethod());
        }

        // (1)
        // Obtain UserName, Password, CompanyId
        String username = super.obtainUsername(request);
        String password = super.obtainPassword(request);
        String companyId = obtainCompanyId(request);
        if (username == null) {
            username = "";
        } else {
            username = username.trim();
        }
        if (password == null) {
            password = "";
        }
        CompanyIdUsernamePasswordAuthenticationToken authRequest =
            new CompanyIdUsernamePasswordAuthenticationToken(username, password, companyId);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest); // (2)
    }

    // (3)
    protected String obtainCompanyId(HttpServletRequest request) {
        return request.getParameter("companyId");
    }
}

項番

説明

(1)
リクエストパラメータから取得した認証情報(ユーザー名、パスワード、会社識別子)より、CompanyIdUsernamePasswordAuthenticationTokenのインスタンスを生成する。
(2)
リクエストパラメータで指定された認証情報(CompanyIdUsernamePasswordAuthenticationTokenのインスタンス)を指定して、org.springframework.security.authentication.AuthenticationManagerauthenticateメソッドを呼び出す。

AuthenticationManagerのメソッドを呼び出すと、AuthenticationProviderの認証処理が呼び出される。
(3)
会社識別子は、companyIdというリクエストパラメータより取得する。

9.2.3.8.4. ログインフォームの修正

ログインフォームの作成で作成したログインフォームに対して、会社識別子を追加する。

<form:form action="${pageContext.request.contextPath}/login" method="post">
    <!-- omitted -->
        <tr>
            <td><label for="username">User Name</label></td>
            <td><input type="text" id="username" name="username"></td>
        </tr>
        <tr>
            <td><label for="companyId">Company Id</label></td>
            <td><input type="text" id="companyId" name="companyId"></td> <!-- (1) -->
        </tr>
        <tr>
            <td><label for="password">Password</label></td>
            <td><input type="password" id="password" name="password"></td>
        </tr>
    <!-- omitted -->
</form:form>

項番

説明

(1)
会社識別子の入力フィールド名にcompanyIdを指定する。

9.2.3.8.5. 拡張した認証処理の適用

ユーザー名、パスワード、会社識別子(独自の認証パラメータ)を使用したDB認証機能をSpring Securityに適用する。

  • SpringSecurityConfig.javaの定義例

// (1)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
        CompanyIdUsernamePasswordAuthenticationFilter companyIdUsernamePasswordAuthenticationFilter) throws Exception {

    http.exceptionHandling(exception -> exception
            .authenticationEntryPoint(loginUrlAuthenticationEntryPoint())); // (1)
    // omitted
    http.addFilterAt(companyIdUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // (2)
    http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository()));
    http.logout(logout -> logout.logoutUrl("/logout")
            .logoutSuccessUrl("/login"));
    // omitted
    http.authorizeHttpRequests(authz -> authz
            .requestMatchers(new AntPathRequestMatcher("/**")).permitAll());

    return http.build();
}

// (3)
@Bean("loginUrlAuthenticationEntryPoint")
public LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint() {
    return new LoginUrlAuthenticationEntryPoint("/login");
}

// (4)
@Bean("companyIdUsernamePasswordAuthenticationFilter")
public CompanyIdUsernamePasswordAuthenticationFilter companyIdUsernamePasswordAuthenticationFilter(
        @Qualifier("authenticationManager") AuthenticationManager authenticationManager) {

    CompanyIdUsernamePasswordAuthenticationFilter bean = new CompanyIdUsernamePasswordAuthenticationFilter();
    bean.setRequiresAuthenticationRequestMatcher(antPathRequestMatcher()); // (5)
    bean.setAuthenticationManager(authenticationManager); // (6)
    bean.setSessionAuthenticationStrategy(sessionAuthenticationStrategy()); // (7)
    bean.setAuthenticationFailureHandler(
            simpleUrlAuthenticationFailureHandler()); // (8)
    bean.setAuthenticationSuccessHandler(
            jspSimpleUrlAuthenticationSuccessHandler()); // (9)
    return bean;
}

// (5)
@Bean("antPathRequestMatcher")
public AntPathRequestMatcher antPathRequestMatcher() {
    return new AntPathRequestMatcher("/authentication", "POST");
}

// (6')
@Bean("authenticationManager")
public AuthenticationManager authenticationManager(
        AuthenticationProvider companyIdUsernamePasswordAuthenticationProvider) {
    return new ProviderManager(companyIdUsernamePasswordAuthenticationProvider);
}

// (6')
@Bean("companyIdUsernamePasswordAuthenticationProvider")
public AuthenticationProvider companyIdUsernamePasswordAuthenticationProvider(
        PasswordEncoder passwordEncoder) {
    CompanyIdUsernamePasswordAuthenticationProvider authProvider = new CompanyIdUsernamePasswordAuthenticationProvider();
    authProvider.setUserDetailsService(customerService());
    authProvider.setPasswordEncoder(passwordEncoder);
    return authProvider;
}

// (7')
@Bean("sessionAuthenticationStrategy")
public CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy() {
    List<SessionAuthenticationStrategy> list = new ArrayList<SessionAuthenticationStrategy>();
    list.add(csrfAuthenticationStrategy());
    list.add(sessionFixationProtectionStrategy());
    CompositeSessionAuthenticationStrategy bean = new CompositeSessionAuthenticationStrategy(list);
    return bean;
}

// (8)
@Bean("simpleUrlAuthenticationFailureHandler")
public SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
    return new SimpleUrlAuthenticationFailureHandler("/login?error=true");
}

// (9)
@Bean("simpleUrlAuthenticationSuccessHandler")
public SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler() {
    return new SimpleUrlAuthenticationSuccessHandler();
}

@Bean("csrfTokenRepository")
public HttpSessionCsrfTokenRepository csrfTokenRepository() {
    return new HttpSessionCsrfTokenRepository();
}

項番

説明

(1)
(2)のHttpSecurity#addFilterAtタグを使用してFORM_LOGIN_FILTERを差し替える場合は、SecurityFilterChainに以下の設定を行う必要がある。
  • HttpSecurity#formLoginは使用できないため、httpSecurity#exceptionHandlingを使用してAuthenticationEntryPointを明示的に指定する。

(2)
HttpSecurity#addFilterAtを使用してFORM_LOGIN_FILTERを差し替える。

HttpSecurity#addFilterAtの第一引数に拡張したAuthentication Filterのbeanを指定し、第二引数に登録済みのFilterクラスを指定する。
上記設定ではFORM_LOGIN_FILTERに該当するUsernamePasswordAuthenticationFilter.classを指定している。
(3)
SecurityFilterChainhttpSecurity#exceptionHandlingに使用するAuthenticationEntryPointのbeanを指定する。

ここでは、HttpSecurity#formLoginを指定した際に使用されるorg.springframework.security.web.authentication.LoginUrlAuthenticationEntryPointクラスのbeanを指定している。
(4)
FORM_LOGIN_FILTERとして使用するAuthentication Filterクラスのbeanを定義する。

ここでは、拡張したAuthentication Filterクラス(CompanyIdUsernamePasswordAuthenticationFilter)のbeanを定義している。
(5)
requiresAuthenticationRequestMatcherプロパティに、認証処理を行うリクエストを検出するためのRequestMatcherインスタンスを指定する。

ここでは、/authenticationというパスにリクエストがあった場合に認証処理を行うように設定している。
これは、HttpSecurity#formLoginloginProcessingUrl/authenticationを指定したのと同義である。
(6)
authenticationManagerプロパティに、AuthenticationManagerのbeanを設定する。
(6’)
Spring Securityが生成するAuthenticationManagerに対して、拡張したAuthenticationProvider(CompanyIdUsernamePasswordAuthenticationProvider)を設定する。
(7)
sessionAuthenticationStrategyプロパティに、認証成功時のセッションの取り扱いを制御するコンポーネント(SessionAuthenticationStrategy)のbeanを指定する。

(7’)
認証成功時のセッションの取り扱いを制御するコンポーネント(SessionAuthenticationStrategy)のbeanを定義する。

ここでは、Spring Securityから提供されている、
  • CSRFトークンを作り直すコンポーネント(CsrfAuthenticationStrategy)

  • セッション・フィクセーション攻撃を防ぐために新しいセッションを生成するコンポーネント(SessionFixationProtectionStrategy)

を有効化している。
(8)
authenticationFailureHandlerプロパティに、認証失敗時に呼ばれるハンドラクラスを指定する。
(9)
authenticationSuccessHandlerプロパティに、認証成功時に呼ばれるハンドラクラスを指定する。

Note

auto-configについて

auto-config="false"を指定又は指定を省略した際にBasic認証処理とログアウト処理を有効化したい場合は、<sec:http-basic>タグと<sec:logout>タグを明示的に定義する必要がある。


9.2.3.9. PasswordEncoderのカスタマイズ

DelegatingPasswordEncoder で使用されるPasswordEncoderインタフェースの実装は全てデフォルトの設定であるが、システムの要件によってはカスタマイズを行う必要がある。
ここではPasswordEncoderインタフェースの実装のカスタマイズ方法を紹介する。

Note

OWASP(Open Web Application Security Project)ではハッシュ関数に使用するイテレーションカウント(ハッシュ化の繰返し回数)について、ユーザに影響を与えない範囲で出来る限り攻撃を対策できる値を設定することが推奨されている。

ハードウェアの処理速度に比例してユーザに影響を与えない範囲が変化するため、実際に設定すべきイテレーションカウントは環境によって異なる。


9.2.3.9.1. Pbkdf2PasswordEncoderのカスタマイズ

Pbkdf2PasswordEncoderでは、ソルトに16バイトの乱数(java.security.SecureRandom)が使用され、イテレーションカウントはデフォルトでは310,000回に設定されている。

Note

  • ソルト

    ハッシュ化対象のデータに追加する文字列のことである。

    ソルトをパスワードに付与することで、実際のパスワードより桁数が長くなるため、レインボークラックなどのパスワード解析を困難にすることができる。なお、ソルトはユーザーごとに異なる値(ランダム値等)を設定することを推奨する。これは、同じソルトを使用していると、ハッシュ値からハッシュ化前の文字列(パスワード)がわかってしまう可能性があるためである。

  • イテレーション

    ハッシュ関数の計算を繰り返し行うことで、保管するパスワードに関する情報を繰り返し暗号化することである。

    パスワードの総当たり攻撃への対策として、パスワード解析に必要な時間を延ばすために行う。しかし、イテレーションはシステムの性能に影響を与えるので、システムの性能を考慮してイテレーションカウントを決める必要がある。

    Spring Securityのデフォルトでは310,000回イテレーションを行うが、この回数はコンストラクタ引数(iterations)で変更することができる。イテレーションカウントが多いほどパスワードの強度は増すが、計算量が多くなるため性能にあたえる影響も大きくなる。


ここではイテレーションカウントの変更方法を紹介する。

  • ApplicationContextConfig.javaの変更

@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
    idToPasswordEncoder.put("pbkdf2", pbkdf2PasswordCustomEncode());
    DelegatingPasswordEncoder bean = new DelegatingPasswordEncoder("pbkdf2", idToPasswordEncoder);
    bean.setDefaultPasswordEncoderForMatches(athnPasswordEncoderBCrypt);
    // omitted
    return bean;
}

// (1)
@Bean
public Pbkdf2PasswordEncoder pbkdf2PasswordCustomEncoder() {
    return new Pbkdf2PasswordEncoder("", 16, 310000, SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256); // (2)(3)(4)(5)
}

項番

説明

(1)
Pbkdf2PasswordEncoderをカスタマイズするため、使用するコンストラクタをファクトリーメソッドから変更する
コンストラクタに指定する引数については以下で解説を行う。
(2)
第一引数(secret) にはハッシュ化で使用する秘密鍵を指定する。
デフォルトは空(””)である。
(3)
第二引数(saltLength) にはハッシュ化対象のデータに追加する文字列長を指定する。
デフォルトは16である。
(4)
第三引数(iterations) にはハッシュ化のiterations(反復)回数を指定する。
デフォルトでは310,000回が設定されている。
(5)
第四引数(secretKeyFactoryAlgorithm) にはハッシュ化アルゴリズムを指定する。
デフォルトではSHA-256が設定されている。

Warning

SecureRandomの使用について

Linux環境でSecureRandomを使用する場合、処理の遅延やタイムアウトが発生する場合がある。 これは使用する乱数生成器に左右される事象であり、以下のドキュメントに説明がある。

詳しくはSecureRandomのjavadocを参照されたい。

本事象が発生する場合は、以下のいずれかの設定を追加することで回避することができる。

  • Javaコマンド実行時に-Djava.security.egd=file:/dev/urandomを指定する。

  • ${JAVA_HOME}/jre/lib/security/java.security内のsecurerandom.source=/dev/randomsecurerandom.source=/dev/urandomに変更する。


9.2.4. Appendix

9.2.4.1. Spring MVCでリクエストを受けてログインフォームを表示する

Spring MVCでリクエストを受けてログインフォームを表示する方法を説明する。

  • ログインフォームを表示するControllerの定義例

@Controller
@RequestMapping("/login")
public class LoginController { // (1)
    @RequestMapping
    public String index() {
        return "login";
    }

}

項番

説明

(1)
view名として”login”を返却する。InternalResourceViewResolverによってsrc/main/webapp/WEB-INF/views/login.jspが出力される。

本例のように、単純にview名を返すだけのメソッドが一つだけあるControllerであれば、<mvc:view-controller>を使用して代用することも可能である。

詳しくは、HTMLを応答するを参照されたい。


9.2.4.2. Remember Me認証の利用

Remember Me認証」とは、Webサイトに頻繁にアクセスするユーザーの利便性を高めるための機能の一つで、ログイン状態を通常のライフサイクルより長く保持するための機能である。
本機能を使用すると、ブラウザを閉じた後やセッションタイムアウトが発生した後でも、Cookieに保持しているRemember Me認証用のTokenを使用して、ユーザ名とパスワードを再入力することなく自動でログインすることができる。なお、本機能は、ユーザーがログイン状態を保持することを許可した場合のみ有効となる。
Spring Securityは、「Hash-Based Token方式のRemember Me認証」と「Persistent Token方式のRemember Me認証」をサポートしており、デフォルトではHash-Based Token方式が使用される。

Remember Me認証を利用する場合は、HttpSecurity#rememberMeを追加する。

  • SpringSecurityConfig.javaの定義例

@Bean
public SecurityFilterChain filterChainJsp2201(HttpSecurity http) throws Exception {

    // omitted
    http.rememberMe(rememberMe -> rememberMe.key("spring-test-km/ylnHv") // (1)
            .tokenValiditySeconds(30 * 24 * 60 * 60)); // (2)
    // omitted
      return http.build();
}

項番

説明

(1)
key属性に、Remember Me認証用のTokenを生成したアプリケーションを識別するキー値を指定する。
キー値の指定が無い場合、アプリケーションの起動毎にユニークな値が生成される。
なお、Hash-Based Tokenが保持しているキー値とサーバーで保持しているキー値が異なる場合、無効なTokenとして扱われる。
つまり、アプリケーションを再起動する前に生成したHash-Based Tokenを有効なTokenとして扱いたい場合は、key属性の指定は必須である。
(2)
token-validity-seconds属性に、Remember Me認証用のTokenの有効時間を秒単位で指定する。
指定が無い場合、デフォルトで14日間が有効時間になる。
上記例では、有効時間として30日間を設定している。
上記以外の属性については、Spring Security Reference -The Security Namespace (<remember-me>) -を参照されたい。

ログインフォームには、「Remember Me認証」機能の利用有無を指定するためのフラグ(チェックボックス項目)を用意する。

  • ログインフォームのJSPの実装例

<form:form action="${pageContext.request.contextPath}/login" method="post">
        <!-- omitted -->
        <tr>
            <td><label for="remember-me">Remember Me : </label></td>
            <td><input name="remember-me" id="remember-me" type="checkbox" checked="checked" value="true"></td> <!-- (1) -->
        </tr>
        <!-- omitted -->
</form:form>

項番

説明

(1)
「Remember Me認証」機能の利用有無を指定するためのフラグ(チェックボックス項目)を追加し、フィールド名(リクエストパラメータ名)には、remember-me-parameterのデフォルト値であるremember-meを指定する。
チェックボックスのvalue属性には、trueを設定する。
チェックボックスをチェック状態にしてから認証処理を実行すると、以降のリクエストから「Remember Me認証」機能が適用される。

9.2.4.3. 非推奨アルゴリズムのPasswordEncoderの利用

セキュリティ要件によっては、前述したPasswordEncoderを実装したクラスでは実現できない場合がある。 特に、既存のアカウント情報で使用しているハッシュ化要件を踏襲する必要がある場合は、前述のPasswordEncoderでは要件を満たせないことがある。

具体的には、既存のハッシュ化要件が以下のようなケースである。

  • アルゴリズムがSHA-512である。

  • ハッシュ化回数が1000回である。

このようなケースでは、PasswordEncoderの実装クラスの一つであるMessageDigestPasswordEncoderを利用することで要件を満たすことができる。

Warning

MessageDigestPasswordEncoderは旧式の実装であり、セキュリティ上のリスクが想定されるため非推奨となっている。

Pbkdf2アルゴリズムやBCryptアルゴリズムを利用することを検討されたい。


9.2.4.3.1. MessageDigestPasswordEncoderの利用

MessageDigestPasswordEncoderはJavaが提供するjava.security.MessageDigestクラスを利用してハッシュ化を行う。
MessageDigestクラスはMD-5、SHA-1、SHA-256等のハッシュアルゴリズムを提供している。
詳細についてはJavadocを参照されたい。

本ガイドラインでは、DelegatingPassowordEncoderを通してMessageDigestPasswordEncoderを利用する方法について説明する。
ここでは以下のハッシュ化要件を満たすPasswordEncoderを実装する。
  • アルゴリズムがSHA-512である。

  • ハッシュ化回数が1000回である。

Note

MessageDigestPasswordEncoderの仕様

MessageDigestPasswordEncoderでハッシュ化を行うと以下のフォーマットで出力される。

  • {salt}hashValue

ソルトはSpring Security 5からランダムに生成するようになったため、安全性が向上している。

ハッシュ化したパスワードに付与されたソルトは照合の際に使用される。

既に固定のソルトを用いてハッシュ化したパスワードについて

先述の通り、Spring Security 5のMessageDigestPasswordEncoderはソルトをランダムに生成するが、既に固定のソルトを用いてパスワードをハッシュ化していた場合も、パスワードにソルトを付与する移行処理を行うことで、照合することができるようになる。

パスワードデータの移行については、MessageDigestPasswordEncoderのJavadocを参照されたい。

この場合、既存のパスワードは固定のソルトを用いて照合が行われるが、パスワードを新規に設定または変更した場合はランダムなソルトが用いられる。


Warning

MessageDigestPasswordEncoderを使用する際は、以下の点に注意する必要がある。

  • 既存のハッシュ値より文字数が多くなる

  • エンコードしたパスワードからソルトの情報が得られる


MessageDigestPasswordEncoderでハッシュ化を行うDelegatingPasswordEncoderのbeanを定義する。

  • ApplicationContextConfig.javaの定義例

// (1)
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
    idToPasswordEncoder.put("MD", passwordEncoderMessageDigest());
    // omitted
    return new DelegatingPasswordEncoder("MD", idToPasswordEncoder);
}

// (2)
@Bean("passwordEncoderMessageDigest")
public MessageDigestPasswordEncoder passwordEncoderMessageDigest() {
    MessageDigestPasswordEncoder bean = new MessageDigestPasswordEncoder("SHA-512"); // (3)
    bean.setIterations(1000); // (4)
    return bean;
}

項番

説明

(1)
ここではbeanのidにpasswordEncoderを指定し、sec:authentication-provider配下に自動的に参照されるように設定する。
(2)
ハッシュ化に使用するPasswordEncoderとしてMessageDigestPasswordEncoderをbean定義する。
(3)
MessageDigestPasswordEncoderで使用するハッシュアルゴリズムを指定する。
ここにはMessageDigestクラスが対応するアルゴリズムを指定することができる。
指定できる値については、Java暗号化アーキテクチャ標準アルゴリズム名のドキュメントを参照されたい。
(4)
ハッシュ化のiterations(反復)回数を指定する。
設定を行わなかった場合、1回となる。

Warning

実際のアプリケーション開発では、セキュリティ上のリスクを軽減するためidForEncodeidToPasswordEncoderkey値にアルゴリズム名を推測できないような値を指定することを推奨する。