Rails2.0 の Cookie Session を既存のJavaアプリに簡単に組み込める Servlet Filter

Rails2.0の Cookie Session Store 面白いですね。詳細については 2.0のcookie session storeを体感する といった記事が詳しいのでそちらに譲ります。

この Cookie Session が RESTfull かどうかといった議論は正直どうでもよくて(個人的にはSessionて概念が発生してる時点でRESTではないと思います)、単純に Session を Cookie に保存するという仕組みのメリットが非常に興味深いと思っています。特に惹かれるのが以下の2点。

アプリケーションサーバの分散に関しては既にあちこちでも言われていますね。Sessionリプリケーションは一切必要なく手軽にサーバを分散できます。それこそ違うデータセンターのサーバでも問題ありません。これから来るであろう大分散化時代にはうってつけではないでしょうか。

スレッド安全性について。これは結構今でも見落とされがちなんですが、Java の ServletAPI を使用したWebアプリケーションでは Session や Session に保存しているオブジェクトを操作する場合にスレッド安全性を確保することが必須となります。F5連打などで同時アクセスを行なった場合に複数のスレッドが単一のSessionオブジェクトにアクセスする可能性があるからです。

ところが、Cookie Session の場合、リクエスト毎に Sessionオブジェクトが独立するのでマルチスレッドに対して考慮する必要がなくなります。尤もその代償に常に後出優先になってしまうので、その点は注意する必要があります。

という訳で、こんな面白い機能は Java の Webアプリでもぜひ使ってみたいと思って車輪の再実装をしてみました。せっかくなので既存のアプリにも適用できるように Servlet Filter として実装しています。Filter の設定を追加するだけで、Session を操作しているコードは一切変更せずに Cookie Session を使用できるようにしています。

普通の Servlet Filter ですので GAE/J上でも動くと思います。GAE/J上で Session 情報が DataStore を占有するような場合にこれを使うことで課金から逃れることができるかもしれません。(CPUコストは上がるのでトレードオフでしょうが)

また、使用にあたり幾つか制限があります。

  • Session ID に関するメソッド郡は使用できません。
    • Session ID が存在しない為。関連するメソッドは全て UnsupportedOperationException を throw します。
  • HttpSessionListener#sessionDestroyed が必ず呼ばれるとは限りません。
    • Sessionオブジェクトはサーバには一切保存されないため、時間監視でSessionオブジェクトを削除するという事ができません。指定した生存期間後にSessionを破棄するのはクライアント側の責務になります。したがって悪意を持ったクライアントが指定した生存期間を超えたSessionオブジェクトを渡してくる可能性があります。ver2.0で生存期間を超えたSessionは受け入れないようにしました。 これが問題になる場合には、HttpSessionActivationListener#sessionDidActivate 等を利用して制御して下さい。HttpSession#invalidate が明示的に呼ばれた場合には必ず HttpSessionListener#sessionDestroyed は呼ばれます。
  • Session に保存するオブジェクトは全て Serializable である必要があります。
    • Cookie には Serialize した結果を書き込みます。直列化不可能なオブジェクトを Session に含める場合、HttpSessionActivationListener 等を用いて直列化可能な状態にしてください。

Rails2.0 の実装との違い

  • 暗号化のサポートをしています。設定で暗号化を有効にすると Cookie の値には暗号化された内容が書き込まれます。個人情報が含まれている場合でも、サーバの秘密鍵が漏洩しない限り安心です。
  • Cookie のサイズ制限が存在するため、シリアライズデータをZLIB圧縮ライブラリで圧縮しています。
  • Cookie を Session の中身と HMAC で2個使用します。同一ドメインで発行できる Cookie数には限りがありますが、データ容量を優先した結果 HMAC と別にしました。

システム条件および依存ライブラリ

  • JavaSE 5.0以上
  • ServletAPI 2.5以上
  • Apache Commons Codec 1.4
  • Apache Commons Logging 1.1

設定方法

web.xml に以下の Filter 設定を追加するだけです。

    <filter>
        <filter-name>CookieSessionFilter</filter-name>
        <filter-class>gakuzo.lab.cookiesession.CookieSessionFilter</filter-class>
        <init-param>
            <!-- Cookieに指定するドメイン。省略した場合、リクエストが送られたドメインになります。 -->
            <param-name>domain</param-name>
            <param-value>localhost</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの secure属性を追加するかどうか。trueの場合、secure属性がつきます。省略時はfalse -->
            <param-name>secure</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <!-- Cookieを識別するアプリケーション名。省略した場合、ContextPath の / を _ に置換した値になります。 -->
            <param-name>applicationName</param-name>
            <param-value>example</param-value>
        </init-param>
        <init-param>
            <!-- Cookieのpath属性に指定する値。省略した場合、ContextPath の値になります。 -->
            <param-name>path</param-name>
            <param-value></param-value>
        </init-param>
        <init-param>
            <!-- HMACの計算に使用するアルゴリズム名。省略した場合、HmacSHA1 になります。 -->
            <param-name>hmacAlgorithmName</param-name>
            <param-value>HmacSHA1</param-value>
        </init-param>
        <init-param>
            <!-- 
                HMACの計算に使用する秘密鍵。省略できません。
                複数のアプリケーションを動かす場合、必ずこの秘密鍵はアプリケーション毎にユニークにしてください。
                さもないと、悪意を持ったクライアントがアプリケーション名を書き換えて別アプリのCookieを渡してきた場合に
                そのCookieを受け入れてしまう可能性があります。
            -->
            <param-name>hmacSecretKey</param-name>
            <param-value>3Z5U4br1lzs8gy/QcPMSPNiT3UOI6UYkE0JNhnT6Sg27VPvWpwrrGeFPI4jOI5A+LO6LXXOhFokt8kD1tWcNZg==</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの値を暗号化するかどうか。省略した場合、false になります。 -->
            <param-name>cryption</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの値を暗号化する時に使用するアルゴリズム名。省略した場合、AES になります。 -->
            <param-name>cryptionAlgorithmName</param-name>
            <param-value>AES</param-value>
        </init-param>
        <init-param>
            <!-- Cookieの値を暗号化する時に使用する秘密鍵。cryption が true の場合は省略できません。 -->
            <param-name>cryptionSecretKey</param-name>
            <param-value>_mPkBsjkKtmax1fSGT0ziA</param-value>
        </init-param>
        <init-param>
            <!-- 
                Session の maxInactiveInterval の初期値です。省略した場合、0 (ブラウザ終了まで)になります。
                Session の maxInactiveInterval と Cookie の maxAge の仕様が異なるため、この Filter では以下のように扱います。
                maxInactiveInterval が負の値の場合、Session はタイムアウトせずに永続化させる必要がありますが、
                Cookie には永続化の指定が存在しないため、maxAge には Integer.MAX_VALUE を指定します。
                maxInactiveInterval が 0 の場合、
                maxAge には Cookie存在期間がブラウザが停止されるまでの間を意味する -1 を指定します。
                この挙動はこの Filter 独自の仕様です。
                maxInactiveInterval が正の値の場合、maxAge には maxInactiveInterval * 60 を指定します。
                Integerの範囲を超えてしまう場合、Integer.MAX_VALUE を指定します。
            -->
            <param-name>defaultMaxInactiveInterval</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <!-- 
                HttpSession に関する EventListener です。ServletAPI の仕様上、Filter からコンテナに登録してある
                EventListner を取得することができません。
                したがって、HttpSessionActivationListener、HttpSessionAttributeListener、HttpSessionListener の指定は
                listener要素ではなくここで指定する必要があります。
                カンマ区切りでクラス名を列挙してください。
            -->
            <param-name>listener</param-name>
            <param-value>
                com.example.FooHttpSessionActivationListener, 
                com.example.BarHttpSessionListener
            </param-value>
        </init-param>
    </filter>
    
    <filter-mapping>
        <filter-name>CookieSessionFilter</filter-name>
        <url-pattern>*</url-pattern>
    </filter-mapping>

Jarファイル

cookie-session-0.2.jar

2010-10-30 00:59 更新

基本的にSessionのタイムアウトCookieの有効期限に任せてたんですが、悪意を持ったクライアントが古いCookieを送信することもある、とのことなので最終アクセス時刻からサーバ側でもチェックするようにしました。

defaultMaxInactiveInterval が0以上に設定されている場合、最終アクセス時刻から現在時刻が defaultMaxInactiveInterval 以上経っていればそのCookieは無効とします。

2011-12-03 18:30 追記

プロジェクトを GitHub に上げました。 https://github.com/gakuzzzz/cookiesession