sessionに有効期限を設定する時の試練

Railsを使っていて自分で直接cookieを設定するという状況はほとんどなく、大抵はsessionをハッシュ感覚で、便利に利用してきた。Rails2.0以降はsessionの保存先はデフォルトでcookieになり、そのまま利用する限りcookieの有効期限は空欄のままなので、ブラウザを終了するまでsessionは保持されることになる。そして、次回ブラウザを起動すると、期限切れのcookie(その中にsessionが保存されている)は削除されている。
ほとんどの場合、上記デフォルト設定のまま使っていたか、またはrestful_authenticationなどに頼りきりだったので、いざ自分でsessionに有効期限を設定しようとした時、苦労してしまった。とても基本的なことであるのに...。

config/environment.rbでの設定

  • 2009-01-01 00:00:00まで有効にしたい場合
ActionController::Base.session_options[:session_expires] = Time.local(2009, 1)
  • 上記のような直接日時を指定する方法で満足できる場合はこれでOK。設定は簡単。
  • しかし、ほとんどの場合は以下のように相対的な日時として指定したくなる。
  • 今から1ヶ月後まで有効にしたい場合
ActionController::Base.session_options[:session_expires] = 1.months.from_now
  • 一見うまく動きそうだが、問題大あり。
    • config/environment.rbはサーバー起動時に1回だけ実行される。
    • よって、上記の設定ではサーバー起動後1ヶ月を経過すると、sessionは常に期限切れの状態となってしまう...。

コントローラーでの設定

  • そこで、app/controllers/application.rbで設定する方法に切り替えてみる。
class ApplicationController < ActionController::Base
...(中略)...
  session :session_expires => 1.months.from_now
  • 良さそうな気がするが、未だ問題あり。
    • development環境では、アクション毎にコントローラーのコードがリロードされ、1.months.from_nowもその都度評価されるのでOKだが、(コードの修正が即反映されて開発には好都合)
    • production環境では、一旦読み込まれたコントローラーのコードは、サーバーが再起動するまで維持されるので、1.months.from_nowも1度しか実行されない。(アクションの反応は素早い)
    • つまり、production環境では、environment.rbでの設定と同じ状況に陥ってしまう...。
  • 上記問題を回避するために、before_filterを利用する方法に切り替えてみる
class ApplicationController < ActionController::Base
...(中略)...
  before_filter :reset_session_expires

  protected
    def reset_session_expires
      ActionController::Base.session_options.update(:session_expires => 1. months.from_now)
    end
  • これでようやく相対日時指定の夢は実現されたようだ。

dynamic_session_expプラグインの利用

$ script/plugin install http://svn.codahale.com/dynamic_session_exp/trunk
  • インストールしたら、1ヶ月後の相対日時指定はconfig/environment.rbで以下のように書ける。
CGI::Session.expire_after 1.month
  • 素晴らしいシンプルさと分かり易さ!自分で頑張るよりも、便利なものは利用させて頂くに限る。

内容が変更されなければ、有効期限も修正されない

  • ホッとしたのも束の間、まだ問題が残っていた。
class MapsController < ApplicationController
  def index
    session[:lat] = params[:lat]
    session[:lon] = params[:lon]
...(中略)...
  • なんと!sessonの内容が同じだと、cookieに対して書き込みが行われないようだ。結果として、有効期限も更新されない...。
  • だから毎回同じページしか表示しない場合、上記の例では1ヶ月後には表示されなくなる。
  • 最終アクセス日時から1ヶ月有効にしたはずなのに、毎日アクセスしていたにも関わらず、突然いつものページが表示されなくなった、ということになってしまう。
  • それを回避するため、reset_sessionを利用したり、毎回変化する値をダミーで設定してみたり...
class MapsController < ApplicationController
  def index
    reset_session
    session[:lat] = params[:lat]
    session[:lon] = params[:lon]
...(中略)...

# または...

class MapsController < ApplicationController
  def index
    session[:updated_at] = Time.now
    session[:lat] = params[:lat]
    session[:lon] = params[:lon]
...(中略)...
  • reset_sessionを実行すると、sessionのキーすべてがリセットされてしまう。
  • 上記の例では、session[:lat], session[:lon]以外のsessionも一旦破棄されてしまうので、注意が必要。
  • ブラウザの環境設定でcookieを表示して確認してみると...
    • session[:updated_at] = Time.now等で内容が更新された時は、有効期限の更新もすぐに確認できた。
    • session_resetではブラウザを再起動するまで、有効期限の更新は確認できなかった。(無駄なcookieアクセスを排除しているのかも)
  • 今回はreset_sessionを利用して、確実に有効期限を更新することにした。

これで、最終アクセス日時から1ヶ月間だけ保持されるようになったかな?
簡単そうに思えることも、やってみると実にいろいろな注意が必要だった。

  • Ajax更新を利用する環境でsession_resetしてしまうと、ことごとくInvalidAuthenticityTokenというエラーに怒られてしまう...。
    • link_to_remote等は常にauthenticity_tokenも送信していて、そのキーをsessionに保存されたキーと照合している。
    • session_resetしてしまうと、そのキーも含めてリセットされてしまうので「authenticity_tokenが不正ですよ」になってしまうのだ。

...ということで、session_resetを利用するのはやめて、session[:updated_at] = Time.nowに変更!(これに気付くまで、結構悩んでしまった...。)