3.3. セッション外部管理

3.3.1. Overview

クラウド環境でオートスケーリングを利用した場合に、スケールイン発生時にセッションなどのアプリケーションサーバインスタンス固有の情報は失われてしまう。 その為、ロードバランサによるロードバランシングにおいてスティッキーセッションはOFFとし、アプリケーションの構造としてどのアプリケーションサーバインスタンスに リクエストが割り振られた場合でも業務継続可能とする為のセッション外部管理方式を示す。


3.3.1.1. セッション外部管理方式

セッション外部管理を行うために、Spring Session with Redisを利用した方式を以下に示す。 Redis構成は、ユーザ数(同時セッション数)が後々スケールできるようシャーディングを用いた構成で紹介している。

Screen image of Session Mamanagement.
項番 説明
(1)
ユーザは、同一のセッションIDでアクセスを行う。
(2)
SessionRepositoryFilterはセッションをラップする。
(3)
Apache TilesTomcatの組み合わせを使用している場合は、SessionEnforcerFilterを使用する必要がある。SessionEnforcerFilterはセッションが存在しない場合は、セッションを作成してリクエストURLにリダイレクトする。セッションが存在する場合は何も実施しない。また、セッションが存在しない場合は、リダイレクトが強制的に発生するため、URLパターンを適切に設定する必要がある。
(4)
アプリケーションで作成したコントローラからgetSessionメソッドなどで、セッションへのアクセスを行った場合に、ラップ済みのセッションを通じてセッションを取得する。
(5)
ラップ済みのセッションは、ローカルサーバのキャッシュにセッション情報が存在しない時はRedisから取得する。一度アクセスを行うと、そのリクエストの間はローカルにセッション情報をキャッシュする。シャーディングされたRedisからの取得は、データに対してkeyのhashを計算して、該当するhash slotにアクセスして取得する。
(6)
レスポンスがコミットされたタイミングでRedisにセッション情報を格納する。

Warning

本ガイドラインで採用しているSpring Session 1.3.1.RELEASEを使用する場合、デフォルトではCookieを用いてセッションIDを参照するCookieHttpSessionStrategyが使用されるが、当該バージョンにはセッションIDを設定したCookieが複数ある場合に、いずれか一つしか採用されない不具合が存在する。そのため、path属性を用いて複数のセッションIDを使い分けるような使い方はできない。詳細はSpring Sessionのissue CookieHttpSessionStrategy should look at all cookies not just the firstを参照されたい。


3.3.1.2. セッション外部管理構成

セッション外部管理を行う為の基本的な構成を以下に示す。

Screen image of Session management.
項番 説明
(1)
ロードバランサのリクエスト振り分けはスティッキーセッションを使用せず、動的なスケーリンググループ内のAPサーバに対して均等に振り分ける。
(2)
アプリケーションではSpring Session with Redisを介してセッションへのアクセスを行う。
(3)
Spring Session with Redisは、Sharding Redis Clusterのいずれかのシャードに対してセッションの保存を行う。各シャードでは、可用性向上のための非同期のレプリケーションが行われる。

3.3.1.3. セッション同期タイミング

リクエスト中に、一度取得したセッション情報はキャッシュされていて、以降はキャッシュからセッション情報を取得する為、他のリクエストでのセッション情報への変更は反映されない。 Redisへ永続化を行うタイミングで各リクエストで行ったセッション情報の変更は上書きで保存されるため、後から永続化が行われたリクエストのセッション情報が反映される。

また、Redisへのセッションの永続化のタイミングは、デフォルトがレスポンスのコミット時となっている。


3.3.1.4. 制約事項

  • セッションの外部管理を行った場合は、「同一セッション内のリクエストの同期化」のような方法でリクエストを同期化することができないため、セッション情報の完全な同期が必要なケースは、セッションで情報を管理しないこと。

    Note

    二重送信防止で、セッションを利用したトランザクショントークンチェックは、トランザクショントークンの変更が即座に同期されないため、リクエストのタイミングに因っては、意図した動作をしないケースが存在する。 そのため、セッションの外部管理を行う場合は、セッションを利用したトランザクショントークンチェックの機能面で制限が発生する点に注意する。 代替手段としては、トランザクショントークンの永続化先をデータベースに変更してロックを使用した排他制御を行うか、アプリケーションを冪等に実装して二重送信が発生しても問題がないようにするとよい(後者の場合は二重送防止処理自体が不要になる)。

    本ガイドラインでは、トランザクショントークンの永続化先をデータベースに変更する拡張方法について説明している。拡張方法については、 TransactionTokenの拡張方法 を参照。

  • Spring Session with Redisは、Keyspace Notificationsを使用してセッション生成・破棄イベントをアプリケーションに通知することが出来る。 イベント通知は全てのアプリケーションサーバに対して行われ、各サーバにおいてHttpSessionListenerが実行されるため、HttpSessionListenerは冪等に実装する必要がある。 また、RedisはKeyspace NotificationsがOFFになっているので、破棄イベントを実装する場合はKeyspace NotificationsをONに設定する必要がある。 詳細は、SessionDeletedEvent and SessionExpiredEventを参照されたい。

  • Servlet仕様では、セッションIDを示すHTTP Cookieの名称は、「JSESSIONID」だが、Spring Sessionを使用した場合のデフォルトは「SESSION」となる。 変更方法は、Spring Session - Custom Cookieを参照されたい。


3.3.1.5. Redis Clusterの一貫性保証

セッション外部管理構成 で説明したとおり、Redis Clusterにおける各シャードでは、マスターノードからスレーブノードへの非同期のレプリケーションが行われている。 以下の条件を満たす場合、データの書き込み完了をクライアントに通知したにもかかわらず、データを失う可能性がある。

  1. クライアントがマスタノードへの書き込み要求を行う。
  2. マスターノードは書き込み処理を行い、書き込み完了をクライアントに通知する。
  3. マスターノードからスレーブノードへのレプリケーションが完了する前にマスターノードがダウンする。
  4. スレーブノードがマスターノードへ昇格する。

この時、レプリケーションされなかったデータについては消失することになる。 また、クライアントが書き込み要求を行っているマスターノードがシャードから分断された場合についても書き込み要求を行っていたノードのダウンが発生するため、レプリケーションが行われなかったデータは消失する。

Redis Clusterを使用したセッションの外部管理を行う場合は、データ消失の可能性がある点に留意すること。

より詳しい情報は、Redis Cluster consistency guaranteesを参照されたい。


3.3.2. How to use

Spring Session with Redisの利用方法を示す。


3.3.2.1. 依存ライブラリの追加

セッション外部管理では、Spring Session with Redisを使用するための依存ライブラリを追加する必要がある。 定義方法は、以下を参照されたい。

  • pom.xml
<dependencies>
        <!-- (1) -->
        <dependency>
                <groupId>org.springframework.session</groupId>
                <artifactId>spring-session</artifactId>
        </dependency>
        <!-- (2) -->
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
</dependencies>
項番 説明
(1)
依存ライブラリにspring-sessionを追加する。
(2)
依存ライブラリにspring-boot-starter-data-redisを追加する。

3.3.2.2. Spring Sessionの設定

セッション外部管理を行うために、Spring Session with Redisを利用する。

  • application.yml
spring:
  session:
    # (1)
    store-type: redis
    # (2)
    timeoutSecond: 1800

  # (3)
  redis:
    listener:
      concurrencyLimit: 2
項番 説明
(1)
spring.session.store-typeにredisを指定する。
(2)
セッションタイムアウトまでの時間を秒で設定する。ここでは、セッションタイムアウトまでの時間を1800秒(30分)に設定している。
(3)
spring.redis.listener.concurrencyLimitにSubscribe処理の際に使用するスレッドの上限を設定する。

  • application-context.xml
<!-- (1) -->
<context:annotation-config/>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
   <!-- (2) -->
   <property name="maxInactiveIntervalInSeconds" value="${spring.session.timeoutSecond}"/>
</bean>
項番 説明
(1)
<context:annotation-config />RedisHttpSessionConfigurationの組み合わせで、springSessionRepositoryFilterのという名前のSpring Beanを作成する。
(2)
RedisHttpSessionConfigurationmaxInactiveIntervalInSecondsapplication.ymlで設定したセッションタイムアウトまでの時間を設定する。

  • xxx-env.xml
<!-- (1) -->
 <bean id="springSessionRedisTaskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor">
     <property name="concurrencyLimit" value="${spring.redis.listener.concurrencyLimit}" />
 </bean>
項番 説明
(1)
RedisMessageListenerContainerが使用するTaskExecutorのBean定義を行う。

Note

RedisMessageListenerContainerは、Subscribe処理の際にspringSessionRedisTaskExecutorのBean名で定義されたTaskExecutorを使用し、Redis上のデータへアクセスを行う。 デフォルトで使用されるSimpleAsyncTaskExecutorはSubscribeの都度、無制限に新規にスレッドを作成し、Redisのコネクションを取得するため、作成されるスレッド数を制限しておくことを推奨する。 上記の例では、デフォルトで使用されるSimpleAsyncTaskExecutorに対してconcurrencyLimitを設定することで、作成されるスレッド数に上限を設定している。


  • web.xml
<!-- (1) -->
<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

・・・

<session-config>
   ...

   <!-- (2) -->
   <session-timeout>30</session-timeout>

   ...
 </session-config>
項番 説明
(1)
DelegatingFilterProxyを使用してspringSessionRepositoryFilterを登録する。また、セッションが存在しない状態でフィルタを通過する前にエラーが発生した場合にもspringSessionRepositoryFilterが適用されるよう、dispatcherERRORも設定する。設定については、XML Servlet Container Initializationを参照されたい。
(2)
セッションタイムアウトの時間は、RedisHttpSessionConfigurationで設定しているので、 web.xmlsession-timeout項目があれば、削除する。

Note

dispatcherに指定する値はシステム要件に応じて全てのリクエストに対してspringSessionRepositoryFilterが適用されるよう設定すること。 例えば、JSPのincludeを行っている場合はdispatcherINCLUDEを追加する必要がある。

Note

DelegatingFilterProxyfilter-nameで指定した名前(上記の例ではspringSessionRepositoryFilter)でDIコンテナからBeanを取得して、処理を委譲する。対象のBeanはFilterを実装する必要がある。Springの下で統一的にFilterが管理でき、コンテナ上の各種Beanを利用してFilterが実装できるなどのメリットがある。

Warning

springSessionRepositoryFilterの登録順序は、HttpSessionを使用する他の Filterより前に登録する必要がある。


3.3.2.3. Apache TilesとTomcatの組み合わせでレスポンスにCookieが設定されない問題の対応

Spring Sessionを使用する際に、Apache TilesTomcatの組み合わせでアプリケーションを作成している場合に、レスポンスにCookieが設定されない問題に対応する必要がある。
この問題に対する詳細は、spring-session/issues/571を参照されたい。

3.3.2.3.1. SessionEnforcerFilter の作成および設定

SessionEnforcerFilter の作成および設定方法を以下に示す。

  • SessionEnforcerFilter.java
public class SessionEnforcerFilter extends OncePerRequestFilter {

   ...

   private RequestMatcher excludeUseSessionRequestMathcer;

   public void setRequestMathcer(RequestMatcher excludeUseSessionRequestMathcer) { //(1)
     this.excludeUseSessionRequestMathcer = excludeUseSessionRequestMathcer;
   }

   @Override
   protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain chain)
                                   throws ServletException, IOException {

      HttpServletRequest httpServletRequest = request;
      HttpServletResponse httpServletResponse = response;

      if (this.excludeRequestMatcher != null
              && this.excludeRequestMatcher.matches(httpServletRequest)) {
          chain.doFilter(httpServletRequest, response);
          return;
      }

      if (httpServletRequest.getRequestedSessionId() == null
              && httpServletRequest.getMethod().toUpperCase().equals("GET")) {

          httpServletRequest.getSession(); //(2)

          StringBuilder requestURI = new StringBuilder(httpServletRequest.getRequestURI());
          if (httpServletRequest.getQueryString() != null) {
              requestURI.append("?").append(httpServletRequest.getQueryString());
          }

          httpServletResponse.sendRedirect(requestURI.toString()); //(3)
      } else {
          chain.doFilter(httpServletRequest, response);
      }

    ...

}
項番 説明
(1)
SessionEnforcerFilterを適用しないpathを設定する。
(2)
セッションIDが送信されず、HTTPリクエストがGETの場合に、 HttpSessionを強制的に作成する。セッションIDが不正な場合や、タイムアウトしたセッションは、後続のフィルターで適切な処理を別途行う必要がある。 詳細は、Warning を参照されたい。
(3)
リクエストを受け付けたパスでリダイレクト実施する。

  • application-context.xml
<!-- (1) -->
<bean id="sessionEnforcerFilter"
    class="com.example.xxx.app.common.session.SessionEnforcerFilter">
    <!-- (2) -->
    <property name="excludeRequestMatcher" ref="excludeEnforceSessionRequestMatcher"/>
</bean>

<!-- (3) -->
<bean id="excludeEnforceSessionRequestMatcher"
    class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
    <constructor-arg value="/health/**"/>
</bean>
項番 説明
(1)
sessionEnforcerFilterを Bean定義する。
(2)
sessionEnforcerFilterを適用しないパス設定を行ったBeanをsessionEnforcerFilterに設定する。
(3)
sessionEnforcerFilterを適用しないパス設定をBean定義する。設定例では、 ヘルスチェック と併用時に、リダイレクトによって死活監視が正常に実施できなくなることを防止するため、ヘルスチェックURL以外にフィルタを適用する為のURLパターンを設定している。

  • web.xml
<!-- (1) -->
 <filter>
     <filter-name>sessionEnforcerFilter</filter-name>
     <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>
 <filter-mapping>
     <filter-name>sessionEnforcerFilter</filter-name>
     <url-pattern>/*</url-pattern>
     <dispatcher>REQUEST</dispatcher>
     <dispatcher>ERROR</dispatcher>
 </filter-mapping>
項番 説明
(1)
sessionEnforcerFilterSpring Sessionの設定 で登録したspringSessionRepositoryFilterの直後に登録する。

Note

本ガイドラインで紹介しているSessionEnforcerFilterはセッションが存在しない場合に、セッションを作成してリクエストURLにリダイレクトする実装を行うことで問題に対応している。 リダイレクトを強制的に発生させているため、システム要件に応じて使用するURLパターンとリダイレクト先のURL不整合が生じないよう留意する必要がある。

Warning

SessionEnforcerFilterを使用する場合は、セッションIDが不正の場合や、セッションがタイムアウトしている可能性があるため、Spring Securityなどで別途セッションタイムアウト検知、CSRF対策を行うことが必要である。Spring Securityでセッションタイムアウト検知を行う設定方法は、 無効なセッションを使ったリクエストの検知 、CSRF対策を行う設定方法は、CSRF対策 を参照されたい。

3.3.2.4. Spring Data Redisの設定

spring-boot-starter-data-redisを使用している為、基本的な設定はAutoConfigurationにて行われる。 詳細な設定については、Spring Boot Reference Guideの Common application propertiesの# REDIS (RedisProperties)を参照されたい。


3.3.2.4.1. エンドポイントの設定

エンドポイント設定は、Spring Data Redisの設定にて定義する。 詳細は、Redis Clusterを参照されたい。

  • application.yml
spring:
  redis:
    cluster:
      # (1)
      nodes:
        - 127.0.0.1:30001
        - 127.0.0.1:30002
        - 127.0.0.1:30003

項番 説明
(1)
spring.redis.cluster.nodesにすべてのノードを追加する。 詳細は、Enabling Redis Clusterを参照されたい。

3.3.2.5. クラウドベンダーの利用

クラウドベンダー提供の環境を利用する場合のガイドラインについて記載箇所を示しておく。

3.3.2.5.1. Amazon Web Service

クラウドベンダーとしてAWSを使用する場合のセッション外部管理については、 セッション外部管理 を参照されたい。

3.3.3. How to extend

本ガイドラインでは、拡張方法や応用的な使用方法を示す。


3.3.3.1. セッション永続化タイミングの変更

セッション永続化のタイミングは、デフォルトでレスポンスのコミット時になっているが、以下の様に定義することでsetAttributeおよびremoveAttributeメソッド呼び出し時に変更することができる。

  • application.yml
spring:
  session:
    redis:
      flush-mode: immediate #(1)

項番 説明
(1)
spring.session.redis.flush-modeimmediateを設定する。デフォルトは、on-saveとなっている。

Warning

immediateを設定する際の注意事項を以下に示す。

  • setAttrubuteの実行回数が多い場合は、頻繁にIOが発生するため性能に影響が出る。
  • setAttrubuteが複数実行される処理に並行し、readしている人が別にいた場合に、変更途中のセッションが読まれてしまう可能性が高まる。
  • getAttributeを使用して取得したオブジェクトに対する変更を行っても永続化は行われない。ただし、他の属性に対するsetAttributeおよびremoveAttributeメソッド実行時に全てのセッション情報が永続化される。

Note

immediateを設定している場合でも、レスポンスのコミット時の永続化は行われる。


3.3.3.2. HttpSessionListenerを利用する場合の設定方法

HttpSessionListenerを使用する場合の設定方法を以下に示す。詳細は、HttpSessionListenerを参照されたい。

  • applicationContext.xml
<!-- (1) -->
<bean class="org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener" />

項番 説明
(1)
使用するHttpSessionListenerをBean定義する。

3.3.3.3. TransactionTokenの拡張方法

Macchinetta Server Framework for Java (1.x) Development Guideline 4.5. 二重送信防止 にて説明しているトランザクショントークンチェックについて、共通ライブラリから提供しているトランザクショントークンチェック機能はトークン情報の格納先をセッションとしている。 そのため、Spring Sessionによるセッションの外部管理を行う場合、セッションの同期化を行うことができないことにより二重送信を防止できないケースがある。 本ガイドラインでは、MyBatis3を使用してトークン情報の格納先をデータベースへ変更する拡張方法について説明する。

実装が必要な要素は以下のとおり。

  • トランザクショントークン情報を格納するテーブル
  • DBアクセスを行うRepositoryインタフェースおよびマッピングファイル
  • トランザクショントークン情報の生成およびテーブルへの格納を行うTransactionTokenStoreインターフェースの実装クラス
  • セッション破棄時にトランザクショントークン情報の削除を行うEventListenerクラス
  • アプリケーションから利用するためのBean定義

3.3.3.3.1. テーブル構成例

本ガイドラインで紹介する拡張方法では、以下のようなテーブルにトランザクショントークン情報を格納する実装を行う。

  • createtable.sql
create table transaction_token (
    token_name varchar(256) not null,
    token_key varchar(32) not null,
    token_value varchar(32) not null,
    session_id  varchar(256) not null,
    sequence bigint,
    constraint pk_transaction_token primary key (token_name, token_key, session_id)
);

create index transaction_token_index_delete_older on transaction_token(token_name, session_id);
create index transaction_token_index_delete_older_sequence on transaction_token(sequence);
create index transaction_token_index_clean on transaction_token(session_id);

create sequence transaction_token_sequence;

3.3.3.3.2. Repositoryインタフェースおよびマッピングファイル

DBアクセスを行うRepositoryインタフェースおよびマッピングファイルを作成する。

  • StoredTransactionTokenRepository.java
public interface StoredTransactionTokenRepository {

    // (1)
    StoredTransactionToken findOneForUpdate(@Param("tokenName") String tokenName, @Param("tokenKey") String tokenKey, @Param("sessionId") String sessionId);

    // (2)
    void delete(@Param("tokenName") String tokenName, @Param("tokenKey") String tokenKey, @Param("sessionId") String sessionId);

    // (3)
    void insert(StoredTransactionToken token);

    // (4)
    void deleteOlderThanLatest(@Param("tokenName") String tokenName, @Param("sessionId") String sessionId, @Param("num") int num);

    // (5)
    void deleteBySessionId(@Param("sessionId") String sessionId);
}
項番 説明
(1)
トークン名およびトークンキーを元にレコードを取得するメソッド。
StoredTransactionTokenは、テーブル構成に対応するEntityクラスである。
(2)
トークン名およびトークンキーを元にレコードを削除するメソッド。
(3)
レコードを1件挿入するメソッド。
(4)
トークン名およびセッションIDを元に、タイムスタンプ降順で指定件数以降のレコードを削除するメソッド。
(5)
セッション破棄時に、セッションIDに紐づくレコードを削除するメソッド。
  • StoredTransactionTokenRepository.xml
<mapper namespace="com.example.domain.repository.StoredTransactionTokenRepository">

        <resultMap id="storedTransactionTokenresultMap" type="StoredTransactionToken">
            <id property="tokenName" column="token_name" />
            <id property="tokenKey" column="token_key" />
            <result property="tokenValue" column="token_value" />
            <result property="sessionId" column="session_id" />
            <result property="sequence" column="sequence" />
        </resultMap>

        <!-- (1) -->
        <select id="findOneForUpdate" resultMap="storedTransactionTokenresultMap">
            <![CDATA[
                SELECT
                    token_name,
                    token_key,
                    token_value,
                    session_id,
                    sequence
                FROM
                    transaction_token
                WHERE
                    token_name = #{tokenName}
                AND
                    token_key = #{tokenKey}
                AND
                    session_id = #{sessionId}
                FOR UPDATE
            ]]>
        </select>

        <!-- (2) -->
        <delete id="delete">
            <![CDATA[
                DELETE FROM transaction_token
                WHERE
                    token_name = #{tokenName}
                AND
                    token_key = #{tokenKey}
                AND
                    session_id = #{sessionId}
            ]]>
        </delete>

        <!-- (3) -->
        <insert id="insert" parameterType="StoredTransactionToken">
            <![CDATA[
                INSERT INTO transaction_token
                (
                    token_name,
                    token_key,
                    token_value,
                    session_id,
                    sequence
                )
                VALUES
                (
                    #{tokenName},
                    #{tokenKey},
                    #{tokenValue},
                    #{sessionId},
                    nextval('transaction_token_sequence')
                )
            ]]>
        </insert>

        <!-- (4) -->
        <delete id="deleteOlderThanLatest">
            <![CDATA[
                DELETE FROM transaction_token
                WHERE sequence IN (
                SELECT sequence FROM transaction_token
                WHERE
                    token_name = #{tokenName}
                AND
                    session_id = #{sessionId}
                ORDER BY sequence DESC
                OFFSET #{num}
                )
            ]]>
        </delete>

        <!-- (5) -->
        <delete id="deleteBySessionId">
            <![CDATA[
                DELETE FROM transaction_token
                WHERE
                    session_id = #{sessionId}
            ]]>
        </delete>
</mapper>
項番 説明
(1)
findOneForUpdateメソッドに対応するSQL。
SELECT FOR UPDATEを使用し、ロックを取得することでトランザクショントークンのチェック処理に対して排他制御を行う。
(2)
deleteメソッドに対応するSQL。
(3)
insertメソッドに対応するSQL。
(4)
deleteOlderThanLatestメソッドに対応するSQL。
(5)
deleteBySessionIdメソッドに対応するSQL。

3.3.3.3.3. TransactionTokenStoreの実装

トークン情報の格納を行うTransactionTokenStoreインターフェースの実装クラスを作成する。 実装する各メソッドの役割については、TransactionTokenStoreインターフェースを参照のこと。

  • MyBatisTransactionTokenStore.java
public class MyBatisTransactionTokenStore implements TransactionTokenStore {

    @Inject
    StoredTransactionTokenRepository tokenRepository;

    @Inject
    JodaTimeDateFactory dateFactory;

    private final int transactionTokenSizePerTokenName;

    private final TokenStringGenerator generator;

    public MyBatisTransactionTokenStore(int transactionTokenSizePerTokenName, TokenStringGenerator generator) {
        this.transactionTokenSizePerTokenName = transactionTokenSizePerTokenName;
        this.generator = generator;
    }

    public MyBatisTransactionTokenStore(int transactionTokenSizePerTokenName) {
        this(transactionTokenSizePerTokenName, new TokenStringGenerator());
    }

    public MyBatisTransactionTokenStore() {
        this(10, new TokenStringGenerator());
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String getAndClear(TransactionToken transactionToken) { // (1)
        String name = transactionToken.getTokenName();
        String key = transactionToken.getTokenKey();
        String sessionId = getSession().getId();

        try {
            StoredTransactionToken token = tokenRepository.findOneForUpdate(name, key, sessionId);
            if (token == null) {
                return null;
            }

            tokenRepository.delete(name, key, sessionId);
            return token.getTokenValue();
        } catch (PessimisticLockingFailureException e) {
        }
        return null;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void remove(TransactionToken transactionToken) { // (2)
        String name = transactionToken.getTokenName();
        String key = transactionToken.getTokenKey();
        String sessionId = getSession().getId();
        tokenRepository.delete(name, key, sessionId);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String createAndReserveTokenKey(String tokenName) { // (3)
        String sessionId = getSession().getId();
        tokenRepository.deleteOlderThanLatest(tokenName, sessionId, transactionTokenSizePerTokenName - 1);
        return generator.generate(UUID.randomUUID().toString());
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void store(TransactionToken transactionToken) { // (4)
        StoredTransactionToken token = new StoredTransactionToken();
        token.setTokenName(transactionToken.getTokenName());
        token.setTokenKey(transactionToken.getTokenKey());
        token.setTokenValue(transactionToken.getTokenValue());
        token.setSessionId(getSession().getId());
        tokenRepository.insert(token);

        getSession();
    }

    HttpSession getSession() {
        return getRequest().getSession(true);
    }

    HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) RequestContextHolder
                .currentRequestAttributes()).getRequest();
    }
}
項番 説明
(1)
getAndClearメソッドを実装する。
データベースに格納したトランザクショントークン情報のレコードをロックした上で取得し、トランザクショントークン情報をデータベースから削除する。
当該メソッドはトランザクショントークンチェック時に動作し、チェックに使用するレコードをロックして排他制御を行うことで、同一のトランザクショントークン情報が複数回使用されないことを保証する。
(2)
removeメソッドを実装する。
当該メソッドは@TransactionTokenCheckを付与したメソッド終了後に動作し、トランザクショントークン情報の削除を行う。
(3)
createAndReserveTokenKeyメソッドを実装する。
当該メソッドは@TransactionTokenCheckを付与したメソッド終了後に動作し、次回チェック用のトランザクショントークン情報の生成を行うとともに、トークン名およびセッションIDに紐づく古い世代のレコードを削除する。
(4)
storeメソッドを実装する。
当該メソッドはトランザクショントークン情報のデータベースへの格納を行う。
セッションが無効になった際に格納されたトランザクショントークン情報の削除を行うため、INSERTを行った後にセッションを取得し、ApplicationEventによる通知を行う。

3.3.3.3.4. HttpSessionListenerの実装

セッション破棄時のHttpSessionDestroyedEventを検知してトランザクショントークン情報の削除を行うEventListenerクラスを作成する。

  • TransactionTokenCleaningListener.java
public class TransactionTokenCleaningListener {

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

    @Inject
    StoredTransactionTokenRepository tokenRepository;

    @EventListener // (1)
    @Transactional
    public void sessionDestroyed(HttpSessionDestroyedEvent event) {
        String sessionId = event.getSession().getId();
        try {
            tokenRepository.deleteBySessionId(sessionId);
            logger.info("Transaction tokens created by sessionId={} have been cleaned.", sessionId);
        } catch (DataAccessException e) {
            logger.warn("Failed to clean abandoned transaction tokens created by sessionId={}.", sessionId, e);
            // ignore
        }
    }
}
項番 説明
(1)
@EventListenerアノテーションを付与し、セッション破棄時にPublishされるHttpSessionDestroyedEventを検知してセッションIDによるトランザクショントークン情報の削除を行うメソッドを実装する。

3.3.3.3.5. アプリケーションでの利用方法

本ガイドラインで紹介する拡張方法を使用した場合においても、ControllerやJSPからの利用方法は同一である。詳細は、トランザクショントークンチェックのControllerでの利用方法 および トランザクショントークンチェックのView(JSP)での利用方法 を参照されたい。

本ガイドラインでは、アプリケーションから利用するためのBean定義方法について説明する。

Macchinetta Server Framework for Java (1.x) Development Guideline 4.5.2.3.5. トランザクショントークンチェックを使用するための設定 にて説明している、HandlerInterceptorの設定について、TransactionTokenInterceptorで使用されるTransactionTokenStoreの実装クラスが作成したMyBatisTransactionTokenStoreとなるようBean定義を行う。

  • spring-mvc.xml
<mvc:interceptor>
    <mvc:mapping path="/**" />
    <mvc:exclude-mapping path="/resources/**" />
    <mvc:exclude-mapping path="/**/*.html" />
    <bean
        class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor">
        <!-- (1) -->
        <constructor-arg index="0">
            <bean class="org.terasoluna.gfw.web.token.TokenStringGenerator" />
        </constructor-arg>
        <constructor-arg index="1">
            <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInfoStore" />
        </constructor-arg>
        <constructor-arg index="2">
            <bean class="com.example.token.MyBatisTransactionTokenStore" />
        </constructor-arg>
    </bean>
</mvc:interceptor>
項番 説明
(1)
TransactionTokenInterceptorのコンストラクタとして、TokenStringGeneratorTransactionTokenInfoStoreおよび作成したMyBatisTransactionTokenStoreを指定する。

HttpSessionListenerによるトークン削除を有効化するため、以下の設定を行う。

  • applicationContext.xml
<!-- (1) -->
<bean id="transactionTokenCleaningListener" class="com.example.token.TransactionTokenCleaningListener" />
項番 説明
(1)
作成したTransactionTokenCleaningListenerのBean定義を行う。