6.3. 認証

6.3.1. Overview

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

Spring Securityでは、設定ファイルの記述のみで、ユーザ認証を実装することができる。 Spring Securityで提供している認証方式として、DB認証、LDAP認証、CAS認証、JAAS認証、X509認証、Basic認証がサポートされているが、本ガイドラインでは、DB認証についてのみ説明する。

Tip

DB認証以外の詳細は、各認証方式の公式ドキュメントを参照されたい。

6.3.1.1. Login

Spring Securityによるログイン処理の流れを以下に示す。

Authentication(Login)
  1. 認証処理を指定したリクエストを受信すると、認証フィルタが起動する。
  2. 認証フィルタは、リクエストからユーザ、パスワードを抽出し、認証情報を生成する。 生成した認証情報をパラメータとし、認証マネージャの認証処理を実行する。
  3. 認証マネージャは、指定された認証プロバイダの認証処理を実行する。 認証プロバイダは、データソース(DBやLDAP)からユーザ情報を取得し、パスワード照合等のユーザ認証を行う。 認証成功時には、認証済みの情報を保持する認証情報を作成し、 認証マネージャに返す。認証失敗の場合は、認証失敗例外を送出する。
  4. 認証マネージャは、受け取った認証情報を認証フィルタに返す。
  5. 認証フィルタは、受け取った認証情報(認証済み)をセッションに格納する。
  6. 認証成功時は、認証前のセッション情報を初期化し、新たにセッション情報を作成する。
  7. 指定された認証成功/失敗時のパスへリダイレクトする。セッションIDをクライアントに返却する。

6.3.1.2. Logout

Spring Securityによるログアウト処理の流れを以下に示す。

Authentication(Logout)
  1. 指定されたログアウト処理へのリクエストを受信すると、ログアウトフィルタが起動する。
  2. ログアウトフィルタはセッション情報を破棄する。 また、クライアントのクッキー(図中のCookie)を破棄するようなレスポンスを設定する。
  3. 指定されたログアウト時のパスへ、リダイレクトする。

Note

ログアウト後、残存するセッション情報が第三者に利用されることによるなりすましを防ぐため、 セッション情報は、ログアウト時にorg.springframework.security.web.session.ConcurrentSessionFilterで破棄される。


6.3.2. How to use

認証機能を使用するために、Spring Securityの設定ファイルに記述する内容を以下に示す。
基本設定については、Spring Security概要を参照されたい。

6.3.2.1. <sec:http>要素の設定

以下の設定例のように、spring-security.xmlの<http>要素のauto-config属性をtrueとすることで、
Spring Securityの認証機能の基本的な設定を、省略することができる。
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:sec="http://www.springframework.org/schema/security"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <sec:http auto-config="true" use-expressions="true">  <!-- (1) -->
      <!-- omitted -->
    </sec:http>
</beans>
項番 説明
(1)
auto-config="true"と設定することで、
<form-login><http-basic><logout>要素を設定しなくても有効になる。

Note

<form-login><http-basic><logout>要素について説明する。

要素名 説明
<form-login>
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilterが有効になる。
UsernamePasswordAuthenticationFilterは、ユーザ名、パスワードをPOST時に、リクエストから取り出し、認証を行うFilterである。
詳細は、<sec:form-login>要素の設定を参照されたい。
<http-basic>
org.springframework.security.web.authentication.www.BasicAuthenticationFilterが有効になる。
BasicAuthenticationFilterは、Basic認証の処理を実施するFilterであり、RFC1945に準拠して実装されている。
詳細な利用方法は、BasicAuthenticationFilter JavaDocを参照されたい。
<logout>
org.springframework.security.web.authentication.logout.LogoutFilter,
org.springframework.security.web.authentication.logout.SecurityContextLogoutHandlerが有効になる。
LogoutFilterは、ログアウト時に呼ばれるFilterであり、
org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices(Cookieの削除) や、
SecurityContextLogoutHandler(セッションの無効化)を呼び出している。
詳細は、<sec:logout>要素の設定を参照されたい。

6.3.2.2. <sec:form-login>要素の設定

本節では、<sec:form-login>要素の設定方法を説明する。

form-login要素の属性について、以下に示す。

spring-security.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:sec="http://www.springframework.org/schema/security"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
  <sec:http auto-config="true" use-expressions="true">
    <sec:form-login login-page="/login"
        default-target-url="/"
        login-processing-url="/authentication"
        always-use-default-target="false"
        authentication-failure-url="/login?error=true"
        authentication-failure-handler-ref="authenticationFailureHandler"
        authentication-success-handler-ref="authenticationSuccessHandler" /> <!-- 属性の指定順番で(1)~(7) -->
  </sec:http>
</beans>
項番 説明
(1)
login-page属性にログインフォーム画面のパスを指定する。
指定がない場合、「/spring_security_login」がデフォルトのパスになり、Spring Securityが用意しているログイン画面が使用される。
「未認証ユーザ」が「認証ユーザ」しかアクセスできないページにアクセスした際に、本パスにリダイレクトされる。
本ガイドラインでは、上記のデフォルト値「/spring_security_login」を使用せず、システム独自の値に変更することを推奨する。この例では”/login”を指定している。
(2)
default-target-url属性に認証成功時の遷移先パスを指定する。
指定がない場合、「/」が、デフォルトのパスになる。
authentication-success-handler-ref属性の指定がある場合、本設定は使用されない。
(3)
login-processing-url属性に認証処理を行うパスを指定する。
指定がない場合、「/j_spring_security_check」がデフォルトのパスになる。
本ガイドラインでは、上記のデフォルト値「/j_spring_security_check」を使用せず、システム独自の値に変更することを推奨する。この例では”/authentication”を指定している。
(4)
always-use-default-target属性に、ログイン成功後にdefault-target-urlに指定したパスに常に遷移するかどうかを設定する。
trueが指定されている場合、default-target-urlに指定したパスに常に遷移する。
false(デフォルト)が指定されている場合、「ログイン前にアクセスしようとした保護ページを表示するためのパス」又は「default-target-urlに指定したパス」のいずれかに遷移する。
authentication-success-handler-ref属性の指定がある場合、本設定は使用されない。
(5)
authentication-failure-urlに認証失敗時の遷移先を設定する。
指定がない場合、login-page属性に指定したパスが適用される。
authentication-failure-handler-ref属性の指定がある場合、本設定は使用されない。
(6)
authentication-failure-handler-ref属性に認証失敗時に呼ばれる、ハンドラクラスを指定する。
詳細は、認証エラー時のハンドラクラスの設定を参照されたい。
(7)
authentication-success-handler-ref属性に認証成功時に呼ばれる、ハンドラクラスを指定する。

上記以外の属性については、Spring Security Reference -The Security Namespace(<form-login>)-を参照されたい。

Warning

Spring Security のデフォルト値「/spring_security_login, /j_spring_security_check」の使用を推奨しない理由

デフォルト値を使用している場合、そのアプリケーションが、Spring Securityを使用していることについて、露見してしまう。 そのため、Spring Securityの脆弱性が発見された場合、脆弱性をついた攻撃を受けるリスクが高くなる。 前述のリスクを避けるためにも、デフォルト値を使用しないことを推奨する。

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

認証時に使用するログインフォームをJSPで作成する。
  • src/main/webapp/WEB-INF/views/login.jsp

    <form:form action="${pageContext.request.contextPath}/authentication" method="post"><!-- (1) -->
        <!-- omitted -->
        <input type="text" id="username" name="j_username"><!-- (2) -->
        <input type="password" id="password" name="j_password"><!-- (3) -->
        <input type="submit" value="Login">
    </form:form>
    
    項番 説明
    (1)
    formのaction属性に認証処理を行うための遷移先を指定する。
    遷移先のパスはlogin-processing-url属性で指定した、/authentication を指定すること。
    ${pageContext.request.contextPath}/authenticationにアクセスすることで認証処理が実行される。
    HTTPメソッドは、「POST」を指定すること。
    (2)
    認証処理において、「ユーザID」として扱われる要素。
    name属性には、Spring Securityのデフォルト値である「j_username」を指定すること。
    (3)
    認証処理において、「パスワード」として扱われる要素。
    name属性には、Spring Securityのデフォルト値である「j_password」を指定すること。

    認証エラーメッセージを表示する場合は以下のコードをJSPに追加する。

    <c:if test="${param.error}"><!-- (1) -->
        <t:messagesPanel
            messagesAttributeName="SPRING_SECURITY_LAST_EXCEPTION"/><!-- (2) -->
    </c:if>
    
    項番 説明
    (1)
    リクエストパラメータに設定されたエラーメッセージの判定を行う。
    form-login要素のauthentication-failure-url属性に設定された値や、
    認証エラーハンドラの”defaultFailureUrl”に設定された値によって、判定処理を変更する必要があるので注意すること。
    本例では、authentication-failure-url=”/login?error=true”のような設定がある場合の、例を示している。
    (2)
    認証エラー時に出力させる例外メッセージを出力する。
    共通ライブラリで提供しているorg.terasoluna.gfw.web.message.MessagesPanelTagを指定して出力させることを推奨する。
    <t:messagesPanel>」タグの使用方法は、メッセージ管理を参照されたい。

Note

認証エラーの例外オブジェクトにJSPからアクセスする際に必要な設定について

認証エラーの例外オブジェクトは、セッションスコープに"SPRING_SECURITY_LAST_EXCEPTION"という属性名で格納されている。 JSPからセッションスコープに格納されているオブジェクトにアクセスするためには、JSPのpageディレクティブのsession属性をtrueにする必要がある。

  • src/main/webapp/WEB-INF/views/common/include.jsp
<%@ page session="true"%>

ブランクプロジェクトのデフォルト設定では、JSPからセッションスコープにアクセスできないようになっている。 これは、安易にセッションが使用されないようにするためである。

  • spring-mvc.xml

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

    <mvc:view-controller path="/login" view-name="login" /><!-- (1) -->
    
    項番 説明
    (1)
    “/login”にアクセスされたら、view名として”login”を返却するだけのControllerを定義する。InternalResourceViewResolverによってsrc/main/webapp/WEB-INF/views/login.jspが出力される。
    この単純なコントローラはJavaによる実装が不要である。

    Tip

    上記の設定は次のControllerと同義である。

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

    単純にview名を返すだけのメソッドが一つだけあるControllerが必要であれば、<mvc:view-controller>を使用すればよい。

6.3.2.2.2. ログインフォームの属性名変更

「j_username」、「j_password」は、Spring Securityのデフォルト値である。<form-login>要素の設定で、任意の値に変更することができる。

  • spring-security.xml

    usernamepasswordの属性

    <sec:http auto-config="true" use-expressions="true">
      <sec:form-login
          username-parameter="username"
          password-parameter="password" /> <!-- 属性の指定順番で(1)~(2) -->
      <!-- omitted -->
    </sec:http>
    
    項番 説明
    (1)
    username-parameter属性でusernameの入力フィールドのname属性を、「username」に変更している。
    (2)
    password-parameter属性でpasswordの入力フィールドのname属性を、「password」に変更している。

6.3.2.3. 認証処理の設定

Spring Securityで認証処理を設定するために、AuthenticationProviderUserDetailsServiceを定義する。

AuthenticationProviderは、次の役割を担う。

  • 認証に成功した場合、認証ユーザー情報を返却する
  • 認証に失敗した場合、例外をスローする

UserDetailsServiceは、認証ユーザー情報を永続化層から取得する役割を担う。

それぞれデフォルトで用意されているものを使用してもよいし、独自拡張して使用しても良い。 組み合わせも自由である。

6.3.2.3.1. AuthenticationProviderクラスの設定

AuthenticationProviderの実装として、DB認証を行うためのプロバイダorg.springframework.security.authentication.dao.DaoAuthenticationProviderを使用する方法を説明する。
  • spring-security.xml

    <sec:authentication-manager><!-- (1) -->
        <sec:authentication-provider user-service-ref="userDetailsService"><!-- (2) -->
            <sec:password-encoder ref="passwordEncoder" /><!-- (3) -->
        </sec:authentication-provider>
    </sec:authentication-manager>
    
    項番 説明
    (1)
    <sec:authentication-manager>要素内に<sec:authentication-provider>要素を定義する。複数指定して、認証方法を組み合わせることが可能であるが、ここでは説明しない。
    (2)
    <sec:authentication-provider>要素でAuthenticationProviderを定義する。デフォルトで、DaoAuthenticationProviderが有効になる。これ以外のAuthenticationProviderを指定する場合はref属性で、対象のAuthenticationProviderのBean IDを指定する

    user-service-ref属性に、認証ユーザ情報を取得するUserDetailsServiceのBean Idを指定する。DaoAuthenticationProviderを使用する場合、この設定は必須である。
    詳細は、UserDetailsServiceクラスの設定を参照されたい。
    (3)
    パスワード照合時に、フォームから入力されたパスワードのエンコードを行うクラスのBean IDを指定する。
    指定がない場合に、「平文」でパスワードが扱われる。詳細は、パスワードハッシュ化を参照されたい。
「ユーザーID」と「パスワード」だけで永続化層からデータを取得し、認証するという要件であればこのDaoAuthenticationProviderを使用すれば良い。
永続化層からのデータ取得方法は次に説明するUserDetailsServiceで決める。

6.3.2.3.2. UserDetailsServiceクラスの設定

AuthenticationProvideruserDetailsServiceプロパティに指定したBeanを設定する。

UserDetailsServiceは次のメソッドをもつインタフェースである。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

このインタフェースを実装すれば、任意の保存場所から認証ユーザー情報を取得することができる。

ここでは、JDBCを使用して、DBからユーザ情報を取得する org.springframework.security.core.userdetails.jdbc.JdbcDaoImplを説明する。

JdbcDaoImplを使用するにはspring-security.xmlに以下のBean定義を行えば良い。

<!-- omitted -->

<bean id="userDetailsService"
  class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
  <property name="dataSource" ref="dataSource"/>
</bean>
JdbcDaoImplは、認証ユーザー情報と認可情報を取得するためのデフォルトSQLを定義しており、これらに対応したテーブルが用意されていることが前提となっている。前提としているテーブル定義はSpring Security Reference -Security Database Schema-を参照されたい。
既存のテーブルからユーザー情報、認可情報を取得したい場合は、発行されるSQLを既存のテーブルに合わせて修正すればよい。
使用するSQLは以下の3つである。
ユーザ情報取得クエリに合致するテーブルを作成することで、後述する設定ファイルへのクエリ指定が不要となる。
「username」、「password」、「enabled」フィールドは必須であるが、
後述する設定ファイルへのクエリ指定で、別名を付与することにより、テーブル名、カラム名が一致しなくても問題ない。
例えば次のようなSQLを設定すれば「email」カラムを「username」として使用することができ、「enabled」は常にtrueとなる。
SELECT email AS username, pwd AS password, true AS enabled FROM customer WHERE email = ?
ログインフォームの作成で前述した、「ユーザID」がクエリのパラメータに指定される。
  • ユーザ権限取得クエリ

    ユーザに対する認可情報を取得するクエリである。
  • グループ権限取得クエリ

    ユーザーが所属するグループの認可情報を取得するクエリである。グループ権限はデフォルトでは無効になっており、本ガイドラインでも扱わない。
以下に、DBの定義例、Spring Securityの設定ファイル例を示す。
テーブルの定義について
DB認証処理を実装するにあたり、必要となるテーブルを定義する。
前述した、デフォルトのユーザ情報取得クエリ合致するテーブルとなっている。
そのため、下記が最低限必要となるテーブルの定義となる(物理名は仮称)。

テーブル名: account

論理名 物理名 説明
ユーザID username 文字列 ユーザを一意に識別するためのユーザID。
パスワード password 文字列 ユーザパスワード。ハッシュ化された状態で格納する。
有効フラグ enabled 真偽値 無効ユーザ、有効ユーザを表すフラグ。「false」に設定されたユーザは無効ユーザとして、認証エラーとなる。
権限名 authority 文字列 認可機能を必要としない場合は不要。

JdbcDaoImplをカスタマイズして設定する例を以下に示す。

<!-- omitted -->

<bean id="userDetailsService"
  class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
  <property name="rolePrefix" value="ROLE_" /><!-- (1) -->
  <property name="dataSource" ref="dataSource" />
  <property name="enableGroups" value="false" /><!-- (2) -->
  <property name="usersByUsernameQuery"
    value="SELECT username, password, enabled FROM account WHERE username = ?" /><!-- (3) -->
  <property name="authoritiesByUsernameQuery"
    value="SELECT username, authority FROM account WHERE username = ?" /><!-- (4) -->
</bean>
項番 説明
(1)
権限名のprefixを指定する。DB上に格納されている権限名が”USER”の場合、この認証ユーザーオブジェクトが持つ権限名は”ROLE_USER”になる。
認可機能と命名規則を合わせて設定する必要がある。認可機能の詳細は、認可を参照されたい。
(2)
認可機能において、「グループ権限」の概念を用いる場合に指定する。
本ガイドラインでは扱わない。
(3)
ユーザ情報を取得するクエリを設定する。取得するデータは、「ユーザID」、「パスワード」、「有効フラグ」の順とする。
「有効フラグ」による認証判定を行わない場合には、「有効フラグ」のSELECT結果を「true」固定とする。
なお、ユーザを一意に取得できるクエリを記述すること。複数件数取得された場合には、1件目のレコードがユーザとして使われる。
(4)
ユーザの権限を取得するクエリを設定する。取得するデータは、「ユーザID」、「権限ID」の順とする。
認可の機能を使用しない場合は、「権限ID」は任意の固定値でよい。

Note

クエリを変更するだけでは実現できない認証を行う場合、UserDetailsServiceを拡張して実現する必要がある。 拡張方法については、UserDetailsServiceの拡張を参照されたい。

6.3.2.4. UserDetailsクラスの利用方法

認証に成功した後にUserDetailsServiceが作成したUserDetailsの利用方法について、説明する。

6.3.2.4.1. JavaクラスでUserDetailsオブジェクトを利用する

認証に成功した後、UserDetailsクラスは
org.springframework.security.core.context.SecurityContextHolderに格納される。

SecurityContextHolderからUserDetailsを取得する例を示す。

public static String getUsername() {
    Authentication authentication = SecurityContextHolder.getContext()
            .getAuthentication(); // (1)
    if (authentication != null) {
        Object principal = authentication.getPrincipal(); // (2)
        if (principal instanceof UserDetails) {
            return ((UserDetails) principal).getUsername(); // (3)
        }
        return (String) principal.toString();
    }
    return null;
}
項番 説明
(1)
SecurityContextHolderからorg.springframework.security.core.Authenticationオブジェクトを取得する。
(2)
AuthenticationオブジェクトからUserDetailsオブジェクトを取得する。
(3)
UserDetailsオブジェクトから、ユーザ名を取得する。

SecurityContextHolderからUserDetailsオブジェクトを取得する方法は、どこからでもstaticメソッドで利用可能であり、 便利な反面、モジュール結合度を高めてしまう。テストも実施しづらい。

UserDetailsオブジェクトは@AuthenticationPrincipalを利用することで取得可能である。
@AuthenticationPrincipalを利用するためにはorg.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver<mvc:argument-resolvers>に設定する必要がある。
  • spring-mvc.xml
 <mvc:annotation-driven>
     <mvc:argument-resolvers>
         <bean
             class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
         <bean
             class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" />
     </mvc:argument-resolvers>
 </mvc:annotation-driven>

Spring MVCのController内では以下のようにSecurityContextHolderを使用せずにUserDetailsオブジェクトを取得できる。

@RequestMapping(method = RequestMethod.GET)
public String view(@AuthenticationPrincipal SampleUserDetails userDetails, // (1)
        Model model) {
    // get account object
    Account account = userDetails.getAccount(); // (2)
    model.addAttribute(account);
    return "account/view";
}
項番 説明
(1)
@AuthenticationPrincipalを利用してログインしているユーザ情報を取得する。
(2)
SampleUserDetailsからアカウント情報を取得する。

Note

@AuthenticationPrincipalアノテーションをつける引数の型はUserDetails型を継承したクラスである必要がある。 通常はUserDetailsServiceの拡張で作成するUserDetails継承クラスを使用すればよい。

SampleUserDetailsクラスはSpring Securityチュートリアルで作成するクラスである。詳細は認証サービスの作成を参照されたい。

Controller内でUserDetailsオブジェクトにアクセスする場合はこちらの方法を推奨する

Note

ServiceクラスではControllerが取得したUserDetailsオブジェクトの情報を使用し、SecurityContextHolderは使用しないことを推奨する。

SecurityContextHolderUserDetailsオブジェクトを引数で渡せないメソッド内でのみ利用することが望ましい。

6.3.2.4.2. JSPでUserDetailsにアクセスする

Spring Securityでは、JSPで認証情報を利用するための仕組みとして、JSP taglibを提供している。このtaglibを使うために以下の宣言が必要である。
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>

Note

TERASOLUNA Server Framework for Java (5.x)の雛形を使用している場合はWEB-INF/views/common/include.jspに設定済みである。

認証ユーザ名をJSPで表示する場合を例に、使用方法を示す。
<sec:authentication property="principal.username" /><!-- (1) -->
項番 説明
(1)
<sec:authentication> タグでAuthenticationオブジェクトにアクセスでき、property属性に指定したプロパティアクセスできる。この例ではgetPrincipal().getUsername()の結果を出力する。
<sec:authentication property="principal" var="userDetails" /> <!-- (1) -->

${f:h(userDetails.username)} <!-- (2) -->
項番 説明
(1)
property属性に指定したプロパティをvar属性にした名前で変数に格納できる。
(2)
(1)で変数に格納した後はJSP内でUserDetailsにアクセスできる。

Note

Controller内でUserDetailsを取得してModelに追加することもできるが、JSPに表示する際はJSPタグを使用すればよい。

Note

UserDetailsServiceクラスの設定で説明したJdbcDaoImplが生成するUserDetailsは「ユーザーID」や「権限」といった最低限の情報しか保持していない。 画面の表示項目として「ユーザー姓名」など他のユーザー情報が必要な場合はUserDetailsUserDetailsServiceを拡張する必要がある。 拡張方法については、UserDetailsServiceの拡張を参照されたい。

6.3.2.5. Spring Securityにおけるセッション管理

ログイン時のセッション情報の生成方式や、例外発生時の設定を行う方法について説明する。
<session-management>タグを指定することで、セッションの管理方法をカスタマイズする事ができる。
以下にspring-security.xmlの設定例を示す。
<sec:http auto-config="true" create-session="ifRequired" ><!-- (1) -->
  <!-- omitted -->
  <sec:session-management
    invalid-session-url="/"
    session-authentication-error-url="/"
    session-fixation-protection="migrateSession" /><!-- 属性の指定順番で(2)~(4) -->
  <!-- omitted -->
</sec:http>
項番 説明
(1)
<http>タグのcreate-session属性には、セッションの作成方針を指定する。
以下の値を指定することができる。
  • always:
    Spring Securityは、既存のセッションがない場合にセッションを新規作成する、セッションが存在すれば、再利用する。
  • ifRequired: (デフォルト)
    Spring Securityは、セッションが必要であれば作成する。セッションがすでにあれば、作成せずに再利用する。
  • never:
    Spring Securityは、セッションを作成しないが、セッションが存在すれば、再利用する。
  • stateless:
    Spring Securityは、セッションを作成しない、セッションが存在しても使用しない。そのため、毎回認証を行う必要がある。
(2)
invalid-session-url属性には、無効なセッションIDがリクエストされた(セッションタイムアウトが発生した)場合に遷移するパスを指定する。
指定しない場合、セッションの存在チェックは実行されずに後続処理が呼び出される。
詳細は、「セッションタイムアウトの検出」を参照されたい。
(3)
session-authentication-error-url属性には、org.springframework.security.web.authentication.session.SessionAuthenticationStrategyで例外が発生した場合に遷移するパスを指定する。
指定しない場合、レスポンスコードに「401 Unauthorized」が設定され、エラー応答が行われる。

本設定は、<form-login>タグを使用して認証を行う場合は使用されない。SessionAuthenticationStrategyで発生した例外は、<form-login>タグのauthentication-failure-url属性 又はauthentication-failure-handler-ref属性の定義に応じてハンドリングされる。
(4)
session-fixation-protection属性には、認証成功時のセッション管理方式を指定する。
以下の値を指定することができる。
  • none
    ログイン前のセッションをそのまま利用する。
  • migrateSession: (Servlet 3.0以前のコンテナ上でのデフォルト)
    ログイン前のセッションを破棄して新しいセッションを新たに作成し、ログイン前のセッションに格納していた情報を引き継ぐ。
  • changeSessionId: (Servlet 3.1以降のコンテナ上でのデフォルト)
    Servlet 3.1から追加されたjavax.servlet.http.HttpServletRequest#changeSessionId()メソッドを使用してセッションIDを変更する。
  • newSession
    ログイン前のセッションを破棄して新しいセッションを新たに作成し、ログイン前のセッションに格納していた情報は引き継がない。
本機能の目的は、新しいセッションIDをログイン毎に割り振ることで、セッション・フィクセーション攻撃を防ぐことにある。そのため、明確な意図がない限り、デフォルト値を使用することを推奨する。

6.3.2.5.1. セッションタイムアウトの検出

セッションタイムアウトを検出したい場合は、 invalid-session-url属性にセッションタイムアウトが発生した際に遷移するパスを指定すればよい。

invalid-session-url属性を指定すると、 http要素のpattern属性に指定したパスパターンに一致する全てのリクエストに対して、 セッションの存在チェック(リクエストされたセッションIDの存在チェック)が行われる。

Note

セッションタイムアウトを検出するパスと検出しないパスが混在する場合は、http要素を複数定義する必要がある。 http要素を複数定義すると、設定が冗長になりメンテナンス性が低下する事があるので注意が必要である。

セッションタイムアウトを検出するために設定が冗長になる場合は、適用パスや除外パスの指定ができるカスタムフィルタを作成することを検討してほしい。 カスタムフィルタを作成する際には、Spring Securityから提供されている以下のクラスを利用又は参考にするとよい。

  • org.springframework.security.web.session.SessionManagementFilter
    セッションの存在チェック(リクエストされたセッションIDの存在チェック)を行う処理が実装されている。
  • org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy
    セッションタイムアウト(無効なセッションID)を検出した後の処理が実装されている。
    デフォルトでは、セッションを生成した後に指定されたパスへリダイレクトする。
  • org.springframework.security.web.util.matcher.RequestMatcher
    リクエストのマッチング判定を行うためのインタフェースであり、適用パスや除外パスの判定処理で利用できる。
    同じパッケージ内にいくつかの便利な実装クラスが提供されている。

Note

<csrf>要素を指定してCSRF対策を行っている場合は、CSRF対策機能によってセッションタイムアウトが検出されるケースがある。

以下に、CSRF対策機能によってセッションタイムアウトが検出される条件を示す。

CSRF対策機能によってセッションタイムアウトが検出された場合は、以下のいずれかの動作となる。

  • invalid-session-url属性の指定がある場合は、セッションを生成した後にinvalid-session-urlに指定したパスへリダイレクトされる。
  • invalid-session-url属性の指定がない場合は、<access-denied-handler>要素に指定したorg.springframework.security.web.access.AccessDeniedHandlerの定義に従ったハンドリングが行われる。

AccessDeniedHandlerの定義方法については、「spring-security.xmlの設定」を参照されたい。


6.3.2.5.2. Concurrent Session Controlの利用設定

Spring Securityでは、1ユーザが同時にログインできるセッション数を制御する機能(Concurrent Session Control)を提供している。
ここでいうユーザとは、Authentication.getPrincipal()で取得される、認証ユーザーオブジェクトのことである。

Note

この機能はアプリケーションサーバが1台構成、またはセッションサーバやクラスタによるセッションレプリケーションを実施している(つまり、全てのアプリケーションが同じセッション領域を利用している)場合に有効である。 複数台または複数インスタンスで構成していて、セッション領域が別々に存在する場合は、本機能では同時ログインを制御できないので注意すること。


最大セッション数を超えた場合の制御方法は、次のパターンが存在する。業務要件によって使い分けること。
  1. 1ユーザの最大セッション数を超過した場合、最も使用されていないユーザを無効にする (後勝ち)
  2. 1ユーザの最大セッション数を超過した場合、新規ログインを受け付けない (先勝ち)

どちらの場合も、この機能を有効にするためにはweb.xmlに以下の設定を追加する必要がある。

<listener>
  <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class><!-- (1) -->
</listener>
項番 説明
(1)
Concurrent Session Control を使用するに当たり、org.springframework.security.web.session.HttpSessionEventPublisherを、listenerに定義する必要がある。

6.3.2.5.3. <sec:concurrency-control>の設定

Concurrent Session Controlを利用する場合は、 <sec:session-management>要素の子要素として<sec:concurrency-control>要素を指定する。

<sec:http auto-config="true" >
  <sec:session-management>
      <sec:concurrency-control
          error-if-maximum-exceeded="true"
          max-sessions="2"
          expired-url="/expiredSessionError.jsp" /><!-- 属性の指定順番で(1)~(3) -->
  </sec:session-management>
</sec:http>
項番 属性名 説明 デフォルト値 デフォルト値説明
(1)
error-if-maximum-exceeded
ログイン可能な最大セッション数を超えている状態でログイン要求があった場合の挙動を指定する。
trueを設定した場合、認証エラーを発生させて、新規ログインを受け付けない。(先勝ち)
false
ログインが可能となり、最も使用されていない(最終アクセス時刻が最も古い)セッションが無効化される。無効化されたセッションを利用しているクライアントからリクエストが発生した場合は、expired-url属性で指定したURLへ遷移する。(後勝ち)
(2)
max-sessions
1ユーザでログイン可能な最大セッション数を指定する。
2を設定した場合、同じユーザで2つのセッションでログインが可能となる。
1
デフォルトは1セッションのみ
(3)
expired-url
無効化されたセッションを利用しているクライアントからリクエストが発生した場合に遷移するURL。
無し
セッションが無効化されたことを通知する固定メッセージが応答される。

Note

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

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

version 1.0.x.RELEASEで依存しているSpring Security 3.1では、org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategyというクラスが提供されていたが、 Spring Security 3.2より非推奨のAPIになっている。 Spring Security 3.1からSpring Security 3.2以降にバージョンアップする場合は、以下のクラスを組み合わせて使用するように変更する必要がある。

  • ConcurrentSessionControlAuthenticationStrategy (Spring Security 3.2で追加)
  • RegisterSessionAuthenticationStrategy (Spring Security 3.2で追加)
  • org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy

具体的な定義方法については、 Spring Security Reference -Web Application Security (Concurrency Control)- のサンプルコードを参考にされたい。


6.3.2.6. 認証エラー時のハンドラクラスの設定

<sec:form-login>要素のauthentication-failure-handler-ref属性に
org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandlerクラスの設定をし、
認証エラー時に送出される例外と、それに対応した遷移先を指定できる。
指定する遷移先は、未認証ユーザがアクセス可能であること。

spring-security.xml

<sec:http auto-config="true" use-expressions="true">
  <sec:form-login login-page="/login"
      authentication-failure-handler-ref="authenticationFailureHandler"
      authentication-success-handler-ref="authenticationSuccessHandler" />
</sec:http>

<bean id="authenticationFailureHandler"
class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/login/defaultError" /><!-- (1) -->
  <property name="exceptionMappings"><!-- (2) -->
    <props>
      <prop key=
        "org.springframework.security.authentication.BadCredentialsException"><!-- (3) -->
          /login/badCredentials
      </prop>
      <prop key=
        "org.springframework.security.core.userdetails.UsernameNotFoundException"><!-- (4) -->
          /login/usernameNotFound
      </prop>
      <prop key=
        "org.springframework.security.authentication.DisabledException"><!-- (5) -->
          /login/disabled
      </prop>
      <prop key=
        "org.springframework.security.authentication.ProviderNotFoundException"><!-- (6) -->
          /login/providerNotFound
      </prop>
      <prop key=
        "org.springframework.security.authentication.AuthenticationServiceException"><!-- (7) -->
          /login/authenticationService
      </prop>
      <!-- omitted -->
    </props>
  </property>
</bean>
項番 説明
(1)
エラー時のデフォルトの遷移先パスを指定する。
後述するexceptionMappingsプロパティに定義していない例外が発生した場合、本プロパティで指定した遷移先に遷移する。
(2)
catchする例外と、例外発生時の遷移先を、リスト形式で指定する。
keyに例外クラスを指定し、値に遷移先を設定する。

Note

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

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

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

  • useForward
  • allowSessionCreation

Spring Securityがスローする代表的な例外を、以下に記述する。

項番 エラーの種類 説明
(3)
BadCredentialsException パスワード照合失敗による認証エラー時にスローされる。
(4)
UsernameNotFoundException
不正ユーザID(存在しないユーザID)による認証エラー時にスローされる。
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProviderを継承したクラスを認証プロバイダに指定している場合、
hideUserNotFoundExceptionsfalseに変更しないと上記例外は、BadCredentialsExceptionに変更される。
(5)
DisabledException 無効ユーザIDによる認証エラー時に、スローされる。
(6)
ProviderNotFoundException
認証プロバイダクラス未検出エラー時にスローされる。
設定誤り等の理由から、認証プロバイダクラスが不正な場合に発生する。
(7)
AuthenticationServiceException
認証サービスエラー時にスローされる。
DB接続エラー等、認証サービス内で何らかのエラーが発生した際に発生する。

Warning

本例では、UsernameNotFoundExceptionをハンドリングして遷移させているが、 ユーザIDが存在しないことを利用者に知らせると、特定のIDの存在有無が判明するため、セキュリティの観点上望ましくない。 そのため、ユーザに通知するメッセージには、例外の種類によって区別をしない画面遷移、メッセージにした方がよい。

6.3.2.7. <sec:logout>要素の設定

本節では、<sec:logout>要素の設定方法を説明する。

spring-security.xml

<sec:http auto-config="true" use-expressions="true">
  <!-- omitted -->
  <sec:logout
      logout-url="/logout"
      logout-success-url="/"
      invalidate-session="true"
      delete-cookies="JSESSIONID"
      success-handler-ref="logoutSuccessHandler"
    /> <!-- 属性の指定順番で(1)~(5) -->
  <!-- omitted -->
</sec:http>
項番 説明
(1)
logout-url属性に、ログアウト処理を実行するためのパスを指定する。
指定がない場合、「/j_spring_security_logout」がデフォルトのパスになる。
本ガイドラインでは、上記のデフォルト値「/j_spring_security_logout」を使用せず、システム独自の値に変更することを推奨する。 この例では”/logout”を指定している。
(2)
logout-success-url属性に、ログアウト後の遷移先パスを指定する。
指定がない場合、「/」がデフォルトのパスになる。
本属性を指定した場合、success-handler-ref属性を指定すると起動時にエラーとなる。
(3)
invalidate-session属性に、ログアウト時にセッションを破棄するかを設定する。
デフォルトはtrueである。
trueの場合、ログアウト時にセッションが破棄される。
(4)
delete-cookies属性に、ログアウト時に削除するクッキー名を列挙する。
複数記述する場合は「,」で区切る。
(5)
success-handler-ref属性に、ログアウト成功後に呼び出すハンドラクラスを指定する。
本属性を指定した場合、logout-success-url属性を指定すると起動時にエラーとなる。

Warning

Spring Security のデフォルト値「/j_spring_security_logout」の使用を推奨しない理由

デフォルト値を使用している場合、そのアプリケーションが、Spring Securityを使用していることについて、露見してしまう。 そのため、Spring Securityの脆弱性が発見された場合、脆弱性をついた攻撃を受けるリスクが高くなる。 前述のリスクを避けるためにも、デフォルト値を使用しないことを推奨する。

Note

CSRF対策で説明している<sec:csrf>を利用している場合は、CSRFトークンチェックが行われるため、ログアウトのリクエストをPOSTで送信し、CSRFトークンも送信する必要がある。 CSRFトークンを埋め込む方法を以下に記述する。

  • CSRFトークンを自動で埋め込む方法

     <form:form method="POST"
       action="${pageContext.request.contextPath}/logout">
       <input type="submit" value="Logout" />
     </form:form>
    

    この場合は以下のようなHTMLが出力される。CSRFトークンがhiddenで設定されている。

    <form id="command" action="/your-context-path/logout" method="POST">
      <input type="submit" value="Logout" />
      <input type="hidden" name="_csrf" value="5826038f-0a84-495b-a851-c363e501b73b" />
    </form>
    
  • CSRFトークンを明示的に埋め込む方法

     <form  method="POST"
       action="${pageContext.request.contextPath}/logout">
       <sec:csrfInput/>
       <input type="submit" value="Logout" />
     </form>
    

    この場合も同様に以下のようなHTMLが出力される。CSRFトークンがhiddenで設定されている。

    <form  method="POST"
      action="/your-context-path/logout">
      <input type="hidden" name="_csrf" value="5826038f-0a84-495b-a851-c363e501b73b" />
      <input type="submit" value="Logout" />
    </form>
    

6.3.2.8. <sec:remember-me>要素の設定

Remeber Me」とは、websiteに頻繁にアクセスするユーザの利便性を、高めるための機能の一つとして、
ログイン状態を保持する機能である。
本機能は、ユーザがログイン状態を保持することを許可していた場合、ブラウザを閉じた後も
cookieにログイン情報を保持し、ユーザ名、パスワードを再入力しなくともログインすることができる機能である。
<sec:remember-me>要素の属性について、以下に示す。

spring-security.xml

<sec:http auto-config="true" use-expressions="true">
  <!-- omitted -->
  <sec:remember-me key="terasoluna-tourreservation-km/ylnHv"
          token-validity-seconds="#{30 * 24 * 60 * 60}" />  <!-- 属性の指定順番で(1)~(2) -->
  <!-- omitted -->
</sec:http>
項番 説明
(1)
key属性に、Remeber Me用のcookieを保持しておくためのユニークなキーを指定する。
指定が無い場合、ユニークなキーを起動時に生成するため、起動時間向上を考えた場合指定しておくことを推奨する。
(2)
token-validity-seconds属性に、Remeber Me用のcookieの有効時間を秒単位で指定する。この例では30日間を設定している。
指定が無い場合、デフォルトで14日間が有効期限になる。

上記以外の属性については、Spring Security Reference -The Security Namespace(<remember-me>)-を参照されたい。

ログインフォームには以下のように「Remeber Me」機能を有効にするためのフラグを用意する必要がある。

<form method="post"
  action="${pageContext.request.contextPath}/authentication">
    <!-- omitted -->
    <label for="_spring_security_remember_me">Remember Me : </label>
    <input name="_spring_security_remember_me"
      id="_spring_security_remember_me" type="checkbox"
      checked="checked"> <!-- (1) -->
    <input type="submit" value="LOGIN">
    <!-- omitted -->
</form>
項番 説明
(1)
HTTPパラメータに、_spring_security_remember_meを設定することで、
trueでリクエストされた場合、次回の認証を回避することができる。

6.3.3. How to extend

6.3.3.1. UserDetailsServiceの拡張

認証時にユーザID、パスワード以外の情報も取得したい場合、
  • org.springframework.security.core.userdetails.UserDetails
  • org.springframework.security.core.userdetails.userDetailsService

を実装する必要がある。

ログインユーザーの氏名や所属部署などの付属情報を常に画面のヘッダーに表示させる必要がある場合、毎リクエストでDBから取得するのは非効率的である。 UserDetailsオブジェクトに保持させて、SecurityContext<sec:authentication>タグからアクセスできようにするにはこの拡張が必要である。

6.3.3.1.1. UserDetailsの拡張

認証情報以外に顧客情報も保持するReservationUserDetailsクラスを作成する。

public class ReservationUserDetails extends User { // (1)
    // omitted

    private final Customer customer; // (2)

    private static final List<? extends GrantedAuthority> DEFAULT_AUTHORITIES = Collections
            .singletonList(new SimpleGrantedAuthority("ROLE_USER"));         // (3)

    public ReservationUserDetails(Customer customer) {
        super(customer.getCustomerCode(),
                customer.getCustomerPassword(), true, true, true, true, DEFAULT_AUTHORITIES); // (4)
        this.customer = customer;
    }

    public Customer getCustomer() { // (5)
        return customer;
    }
}
項番 説明
(1)
UserDetailsのデフォルトクラスである、org.springframework.security.core.userdetails.Userクラスを継承する。
(2)
認証情報および顧客情報をもつDomainObjectクラスを保持する。
(3)
認可情報を、org.springframework.security.core.authority.SimpleGrantedAuthorityのコンストラクタで作成する。ここでは”ROLE_USER”という権限を与える。

本実装は簡易実装であり、本来は認可情報はDB上の別のテーブルから取得すべきである。
(4)
スーパークラスのコンストラクタに、DomainObjectが持つユーザID、パスワードを設定する。
(5)
UserDetails経由で顧客情報にアクセスするためのメソッド。

Note

Userクラスを継承するだけでは、業務要件を実現できない場合、UserDetailsインタフェースを実装すればよい。

6.3.3.1.2. 独自UserDetailsServiceの実装

UserDetailsServiceを実装したReservationUserDetailsServiceクラスを作成する。
本例では、Customerオブジェクトを取得する処理を実装したCustomerSharedServiceクラスをインジェクションして、DBから顧客情報を取得している。
public class ReservationUserDetailsService implements UserDetailsService {
    @Inject
    CustomerSharedService customerSharedService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Customer customer = customerSharedService.findOne(username);
        // omitted
        return new ReservationUserDetails(customer);
    }

}

6.3.3.1.3. 使用方法

作成したReservationUserDetailsServiceReservationUserDetailsの使用方法を説明する。

  • spring-security.xml

    <sec:authentication-manager>
        <sec:authentication-provider user-service-ref="userDetailsService"><!-- (1) -->
            <sec:password-encoder ref="passwordEncoder" />
        </sec:authentication-provider>
    </sec:authentication-manager>
    
    <bean id="userDetailsService"
        class="com.example.domain.service.userdetails.ReservationUserDetailsService"><!-- (2) -->
    </bean>
    <!-- omitted -->
    
    項番 説明
    (1)
    ReservationUserDetailsServiceのBean IDをref属性に定義する。
    (2)
    ReservationUserDetailsServiceをBean定義する。
  • JSP

    <sec:authentication>タグを使用してCustomerオブジェクトにアクセスする。

    <sec:authentication property="principal.customer" var="customer"/><!-- (1) -->
    ${f:h(customer.customerName)}<!-- (1) -->
    
    項番 説明
    (1)
    ReservationUserDetailsがもつCustomerオブジェクトを変数に格納する。
    (2)
    変数に格納したCustomerオブジェクトの任意のプロパティを表示する。
    f:h()については、XSS対策を参照されたい。
  • Controller

    @RequestMapping(method = RequestMethod.GET)
    public String view(@AuthenticationPrincipal ReservationUserDetails userDetails, Model model) {
        // get Customer
        Customer customer = userDetails.getCustomer(); // (1)
        // omitted ...
    }
    
    項番 説明
    (1)
    ReservationUserDetailsから、ログイン中のCustomerオブジェクトを取得する。
    このオブジェクトをServiceクラスに渡して業務処理を行う。

Note

顧客情報が変更された場合、一度ログアウトしないとReservationUserDetailsがもつCustomerオブジェクトは変更されない。

頻繁に変更されうる情報や、ログインユーザー以外のユーザー(管理者など)によって変更される情報は保持しない方がよい。


6.3.3.2. AuthenticationProviderの拡張

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を生成するためのサーブレットフィルタクラス。

ここでは、org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilterクラスを継承して作成する。

Tip

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

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


6.3.3.2.1. UsernamePasswordAuthenticationTokenの拡張

ここでは、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)
認証済みの情報を保持するインスタンスを作成する際に使用するコンストラクタを作成する。
親クラスのコンストラクタの引数に認可情報を渡すことで、認証済みの状態となる。

6.3.3.2.2. DaoAuthenticationProvideの拡張

ここでは、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が指定された場合に、 本クラスを使用して認証処理を行うようにする。

Tip

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


6.3.3.2.3. UsernamePasswordAuthenticationFilterの拡張

ここでは、UsernamePasswordAuthenticationFilterクラスを継承し、 認証情報(ユーザ名、パスワード、会社識別子)をAuthenticationProviderに引き渡すためのサーブレットフィルタクラスを作成する。

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"というリクエストパラメータより取得する。

Note

認証情報の入力チェックについて

DBサーバへの負荷軽減等で、あきらかな入力誤りに対しては、事前にチェックを行いたい場合がある。 その場合は、UsernamePasswordAuthenticationFilterの拡張のように、 UsernamePasswordAuthenticationFilterを拡張することで、入力チェック処理を行うことができる。

なお、上記例では入力チェックは行っていない。

Todo

認証情報の入力チェックは、Controllerクラスでリクエストをハンドリングして、Bean Validationを使用して行う事も可能である。

Bean Validationを使用した入力チェックの方法については、今後追加する予定である。


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

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

spring-security.xml

<!-- omitted -->

<!-- (1) -->
<sec:http
    auto-config="false"
    use-expressions="true"
    entry-point-ref="loginUrlAuthenticationEntryPoint">

    <!-- omitted -->

    <!-- (2) -->
    <sec:custom-filter
        position="FORM_LOGIN_FILTER"
        ref="companyIdUsernamePasswordAuthenticationFilter" />

    <!-- omitted -->

    <sec:csrf token-repository-ref="csrfTokenRepository" />

    <sec:logout
        logout-url="/logout"
        logout-success-url="/login"
        delete-cookies="JSESSIONID" />

    <!-- omitted -->

    <sec:intercept-url pattern="/login" access="permitAll" />
    <sec:intercept-url pattern="/**" access="isAuthenticated()" />

    <!-- omitted -->

</sec:http>

<!-- (3) -->
<bean id="loginUrlAuthenticationEntryPoint"
    class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg value="/login" />
</bean>

<!-- (4) -->
<bean id="companyIdUsernamePasswordAuthenticationFilter"
    class="com.example.app.common.security.CompanyIdUsernamePasswordAuthenticationFilter">
    <!-- (5) -->
    <property name="requiresAuthenticationRequestMatcher">
        <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
            <constructor-arg index="0" value="/authentication" />
            <constructor-arg index="1" value="POST" />
        </bean>
    </property>
    <!-- (6) -->
    <property name="authenticationManager" ref="authenticationManager" />
    <!-- (7) -->
    <property name="sessionAuthenticationStrategy" ref="sessionAuthenticationStrategy" />
    <!-- (8) -->
    <property name="authenticationFailureHandler">
        <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
            <constructor-arg value="/login?error=true" />
        </bean>
    </property>
    <!-- (9) -->
    <property name="authenticationSuccessHandler">
        <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler" />
    </property>
</bean>

<!-- (6') -->
<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="companyIdUsernamePasswordAuthenticationProvider" />
</sec:authentication-manager>
<bean id="companyIdUsernamePasswordAuthenticationProvider"
    class="com.example.app.common.security.CompanyIdUsernamePasswordAuthenticationProvider">
    <property name="userDetailsService" ref="sampleUserDetailsService" />
    <property name="passwordEncoder" ref="passwordEncoder" />
</bean>

<!-- (7') -->
<bean id="sessionAuthenticationStrategy"
    class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
    <constructor-arg>
        <util:list>
            <bean class="org.springframework.security.web.csrf.CsrfAuthenticationStrategy">
                <constructor-arg ref="csrfTokenRepository" />
            </bean>
            <bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
        </util:list>
    </constructor-arg>
</bean>

<bean id="csrfTokenRepository"
    class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository" />


<!-- omitted -->
項番 説明
(1)

custom-filter要素を使用して”FORM_LOGIN_FILTER”を差し替える場合は、http要素の属性に、以下の設定を行う必要がある。

  • 自動設定を使用することができないため、auto-config="false"を指定するか、auto-config属性を削除する。
  • form-login要素を使用できないため、entry-point-ref属性を使用して、使用するAuthenticationEntryPointを明示的に指定する。
(2)

custom-filter要素を使用して”FORM_LOGIN_FILTER”を差し替える。

custom-filter要素のposition属性に"FORM_LOGIN_FILTER"を指定し、ref属性に拡張したサーブレットフィルタのbean IDを指定する。

(3)

http要素のentry-point-ref属性に指定するAuthenticationEntryPointのbeanを定義する。

ここでは、form-login要素を指定した際の使用されるorg.springframework.security.web.authentication.LoginUrlAuthenticationEntryPointクラスのbeanを定義している。

(4)

“FORM_LOGIN_FILTER”として使用するサーブレットフィルタのbeanを定義する。

ここでは、拡張したサーブレットフィルタクラス(CompanyIdUsernamePasswordAuthenticationFilter)のbeanを定義している。

(5)

requiresAuthenticationRequestMatcherプロパティに、認証処理を行うリクエストを検出するためのRequestMatcherインスタンスを指定する。

ここでは、/authenticationというパスにリクエストがあった場合に認証処理を行うように設定している。 これは、form-login要素のlogin-processing-url属性に"/authentication"を指定したのと同義である。

(6)

authenticationManagerプロパティに、authentication-manager要素のalias属性に設定した値を指定する。

authentication-manager要素のalias属性を指定すると、 Spring Securityが生成したAuthenticationManagerのbeanを、他のbeanへDIすることができる様になる。

(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="false"を指定した場合、<sec:http-basic>要素と<sec:logout>要素は、明示的に定義しないと有効にならない。


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

ここでは、ログインフォームの作成で紹介した画面(JSP)に対して、会社識別子を追加する。

<form:form action="${pageContext.request.contextPath}/authentication" method="post">
    <!-- omitted -->
    <span>User Id</span><br>
    <input type="text" id="username" name="j_username"><br>
    <span>Company Id</span><br>
    <input type="text" id="companyid" name="companyid"><br>  <!-- (1) -->
    <span>Password</span><br>
    <input type="password" id="password" name="j_password"><br>
    <!-- omitted -->
</form:form>
項番 説明
(1)
会社識別子の入力フィールド名に、"companyid"を指定する。

6.3.4. Appendix

6.3.4.1. 遷移先の指定が可能な認証成功ハンドラ

Spring Securityを使用した認証では、認証に成功した場合は、

  • bean定義ファイル(spring-security.xml)に記述したパス(<form-login>要素のdefault-target-url属性に指定したパス)
  • ログイン前にアクセスした「認証が必要な保護ページ」を表示するためのパス

に遷移する。

共通ライブラリでは、Spring Securityが提供している機能に加えて、 遷移先のパスをリクエストパラメータで指定できるクラス(org.terasoluna.gfw.security.web.redirect.RedirectAuthenticationHandler)を提供している。

RedirectAuthenticationHandlerは、以下のような仕組みを実現するために作成されたクラスである。

  • ページを表示するためにログインを行う必要がある
  • ログイン後の遷移先のページをJSP側(遷移元のJSP)で指定したい
Authentication_Appendix_Screen_Flow

Picture - Screen_Flow

RedirectAuthenticationHandlerの使用例を、下記に示す。

遷移元画面のJSPの記述例

<form:form action="${pageContext.request.contextPath}/login" method="get">
    <!-- omitted -->
  <input type="hidden" name="redirectTo"
    value="${pageContext.request.contextPath}/reservetour/read?
    ${f:query(reserveTourForm)}&page.page=${f:h(param['page.page'])}
    &page.size=${f:h(param['page.size'])}" />  <!-- (1) -->
</form:form>
項番 説明
(1)
hidden項目として、「ログイン成功後に遷移するページのURL」を設定する。
hidden項目のフィールド名(リクエストパラメータ名)は「redirectTo」を指定する。

フィールド名(リクエストパラメータ名)は、RedirectAuthenticationHandlertargetUrlParameterプロパティの値と一致させる必要がある。

ログイン画面のJSPの記述例

<form:form action="${pageContext.request.contextPath}/authentication" method="post">
     <!-- omitted -->
     <input type="submit"
       value="Login">
     <input type="hidden" name="redirectTo" value="${f:h(param.redirectTo)}" />  <!-- (1) -->
     <!-- omitted -->
</form:form>
項番 説明
(1)
hidden項目として、遷移元画面からリクエストパラメータで渡された「ログイン成功後に遷移するページのURL」を設定する。
hidden項目のフィールド名(リクエストパラメータ名)は「redirectTo」を指定する。

フィールド名(リクエストパラメータ名)は、RedirectAuthenticationHandlertargetUrlParameterプロパティの値と一致させる必要がある。

Spring Security 設定ファイル

<sec:http auto-config="true">
    <!-- omitted -->
    <!-- (1) -->
    <sec:form-login
        login-page="/login"
        login-processing-url="/authentication"
        authentication-failure-handler-ref="authenticationFailureHandler"
        authentication-success-handler-ref="authenticationSuccessHandler" />
    <!-- omitted -->
</sec:http>

<!-- (2) -->
<bean id="authenticationSuccessHandler"
    class="org.terasoluna.gfw.security.web.redirect.RedirectAuthenticationHandler">
</bean>

<!-- (3) -->
<bean id="authenticationFailureHandler"
    class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="/login?error=true"/> <!-- (4) -->
    <property name="useForward" value="true"/> <!-- (5) -->
</bean>
項番 説明
(1)
authentication-failure-handler-ref(認証エラー時のハンドラ設定)とauthentication-success-handler-ref(認証成功時のハンドラ設定)のBeanIdを指定する。
(2)
authentication-success-handler-refから参照されるbeanとしてorg.terasoluna.gfw.security.web.redirect.RedirectAuthenticationHandlerを定義する。
(3)
authentication-failure-handler-refから参照されるbeanとしてorg.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandlerを定義する。
(4)
認証失敗時の遷移先パスを指定する。
上記例では、ログイン画面のパスと認証エラー後の遷移であることを示すクエリ(error=true)を設定している。
(5)
本機能を使用する場合はuseForwardをtrueに指定する必要がある。
trueに指定することで、認証失敗時に表示する画面(ログイン画面)に遷移する際に、RedirectではなくForwardが使用される。
これは、認証処理を行うリクエストのリクエストパラメータの中に「ログイン成功後に遷移するページのURL」を含める必要があるためである。
Redirectを使用して認証エラー画面を表示してしまうと、「ログイン成功後に遷移するページのURL」がリクエストパラメータから引き継ぎことが出来ないため、ログインが成功しても指定した画面に遷移することが出来ない。
この事象を回避するためには、Forwardを使用して「ログイン成功後に遷移するページのURL」をリクエストパラメータから引き継げるようにしておく必要がある。

Tip

RedirectAuthenticationHandlerは、オープンリダイレクタ脆弱性対策が施されているため、 「http://google.com」のような外部サイトへ遷移をすることはできない。 外部サイトへ移動したい場合は、org.springframework.security.web.RedirectStrategyを実装したクラスを作成し、 RedirectAuthenticationHandlertargetUrlParameterRedirectStrategyプロパティにインジェクションすることで実現する事ができる。

拡張する際の注意点としては、redirectToの値を改竄されても問題が発生しないようにする必要がある。 たとえば、以下のような対策が考えられる。

  • 遷移先のURLを直接指定するのではなく、ページ番号などのIDを指定してIDに対応するURLにリダイレクトする。
  • 遷移先のURLをチェックし、ホワイトリストに一致するURLのみリダイレクトする。