Sinatra で簡単なサイトを構築しようと思いつつ、理解力がないためになかなか進まない[twitter:@riocampos]です。
Session?
再度書きますが私的理解です。誤解を含んでいるかも知れません。
- Ruby on Rails でも Sinatra でも、Rack というものの上に立っている。
- Web サイトではセッションがないとサイト遷移により情報が潰えてしまう。
- セッション情報の保持には、Rack の場合 Rack::Session 以下のクラスで扱っているものを使用する。
- ネットなどでいちばん良く説明されているのが Rack::Session::Cookie だが、セッション情報の全てを Cookie へ入れてしまうので盗まれた場合の危険性が高い。 Rack::Session::Memcache では、セッション ID のみ cookie に、その他のセッション情報が memcached に保存される(もちろん memcached が動いている必要がある)。 Rack::Session::Pool でも同様に、セッション ID のみ cookie に、その他のセッション情報がインスタンス変数
@pool
に保持される(当然ながらアプリを終えれば消える)。
- Rack::Session::Cookie
→ セッションID、及び全てのキー・バリューのペアをCookieに保存する。
- Rack::Session::Pool
→ セッションIDのみクッキーに保存する。データはRack::Session::Poolのインスタンス変数@poolに、メモリ上で永続化の処理をせず(つまりそのまま)保存される。そのため高速で動作し、かつ保存できるオブジェクトに(永続化しないため)制約がない。ただし、アプリケーションを再起動した場合にはデータは全て失われる。
- Rack::Session::Memcache
→ セッションIDのみクッキーに保存する。データはmemcachedにより保存するため、当然memcachedは導入済みである必要がある。おそらく、永続化の処理は入るため保存できるデータの制約はRack::Session::Cookieと一緒と思われる。高速で動作する。速度的にはRack::Session::Poolとほぼ同等(永続化の操作の分わずかに劣ると思うが)。アプリケーションを再起動してもデータは保持される。
速度性能を追求するかどうか、そしてアプリケーションの再起動時にデータを保存する必要があるかどうか、が使い分けのポイントですかね。
河西 高明 Tech Blog: Rack::Session::PoolとRack::Session::Cookieの違い
その他、保存先が MongoDB になる rack-session-mongo gem というのもあるようです*1。API の構成は Rack::Session::Memcache や Rack::Session::Pool と同じなので、使い方に悩むことは全くないと思います。
情報源:ruby - Using `Rack::Session::Pool` over `Rack::Session::Cookie` - Stack Overflow
6/18追記:保存先が redis になる gemが二つ。
◎rack-session-redis gem情報源:sinatraでセッション管理をredisでやろうとしたらちょっと困った話 - blog.youyo.info
◎redis-store gemと redis-rack gem
- redis-store/redis-store
- File: README ― Documentation for redis-store (1.1.4)
- redis-store/redis-rack
- File: README ― Documentation for redis-rack (1.5.0)
情報源:セッションの保存先にRedisを使う - #詰んでる日記
このgemを使うとRailsとかSinatraとかRackとかのキャッシュやセッションの保存先にRedisを使うようにすることができる。
ただしこれらの gem も更新されていないので、動くかどうか確認しましょう。
6/18追記ここまで
Sinatra での Sessionの使い方
enable :sessions
を使えば良いよ、ということ。
session ハッシュに何か入れると、Rack::Session::Cookie により cookie を通じてセッションを越えて情報を送れます。
How do I use sessions?
Sessions are disabled by default. You need to enable them and then use the session hash from routes and views:
enable :sessions get '/foo' do session[:message] = 'Hello World!' redirect to('/bar') end get '/bar' do session[:message] # => 'Hello World!' endIf you need to set additional parameters for sessions, like expiration date, use Rack::Session::Cookie directly instead of enable :sessions (example from Rack documentation):
use Rack::Session::Cookie, :key => 'rack.session', :domain => 'foo.com', :path => '/', :expire_after => 2592000, # In seconds :secret => 'change_me'
で。
enable :sessions
だけだと必ず警告が出ます。
SECURITY WARNING: No secret option provided to Rack::Session::Cookie. This poses a security threat. It is strongly recommended that you provide a secret to prevent exploits that may be possible from crafted cookies. This will not be supported in future versions of Rack, and future versions will even invalidate your existing user cookies.
なので、secret オプションを付けたいですよね。
1. Sinatra の範囲で secret オプションを設定
enable :sessions
の代わりに
set :sessions, secret: 'set_your_secret_key'
と設定します。
参考:SinatraでSessionを利用する · twopipe
ちなみに、enable :sessions
は
set :sessions, true
のシンタックスシュガーです。
参考:Sinatra - Configuration の邦訳 | FreeStyleVision
2. Rack の範囲で secret オプションを設定
enable :sessions
の代わりに
use Rack::Session::Cookie, secret: 'set_your_secret_key'
と設定します。
Rack::Session::Memcache / Rack::Session::Pool、または rack-session-mongo gem を導入して Rack::Session::Mongo を使う場合、などでは、この use Rack::Session::Cookie
の代わりにそれぞれのクラスを指定すれば良いでしょう。
ちなみに Rack で用いられる
use Rack::Session::Cookie, ...
の use
ですが、
useはRack::Builderで用意されたDSLの記法で,後続のミドルウェアやアプリケーションを引数に取ってnewしてHandlerの引数にする,ということをおこなっています。
第25回 Rackとは何か(3)ミドルウェアのすすめ:Ruby Freaks Lounge|gihyo.jp … 技術評論社
ということだそうです。
Rack::Session::Pool を使う例('15/2/21追記)
use Rack::Session::Pool, path: '/', domain: nil, expire_after: 60 * 10, # Cookieの有効期限を10分に secret: Digest::SHA256.hexdigest(rand.to_s)
余談ですが Digest::SHA1.hexdigest(rand.to_s)
はもう古いので止めましょうね。
cookieの中身を確認
Sinatra 本家の FAQ に倣って、次のようなスクリプトをローカルで動かしてみます。
require 'sinatra' require 'sinatra/reloader' require 'pp' set :sessions, secret: 'xxx' get '/foo' do session[:message] = 'Hello World!' pp session redirect to('/bar') end get '/bar' do pp session session[:message] # => 'Hello World!' end
Chrome のシークレットウィンドウで http://localhost:4567/foo へアクセスすると即座に http://localhost:4567/bar へ転送されます。
そのときのログはこのようになりました(読みやすいように pp session
の出力に改行を加えました)。
{ "session_id"=>"30071129b53a4fda0a5b66432fd9e3eeaa1cc124b1aa30b7e35ffac41f07e768", "csrf"=>"9eaa397b3689b0c09c20e339b8f95618", "tracking"=>{ "HTTP_USER_AGENT"=>"9637f8f4698bacb96d868e6d21c5775564123002", "HTTP_ACCEPT_LANGUAGE"=>"2668ccc0b26c1ada1e450bf13e21e51501a40422" }, "message"=>"Hello World!" } 127.0.0.1 - - [16/Jun/2014 16:44:43] "GET /foo HTTP/1.1" 302 - 0.0013 { "session_id"=>"30071129b53a4fda0a5b66432fd9e3eeaa1cc124b1aa30b7e35ffac41f07e768", "csrf"=>"9eaa397b3689b0c09c20e339b8f95618", "tracking"=>{ "HTTP_USER_AGENT"=>"9637f8f4698bacb96d868e6d21c5775564123002", "HTTP_ACCEPT_LANGUAGE"=>"2668ccc0b26c1ada1e450bf13e21e51501a40422" }, "message"=>"Hello World!" } 127.0.0.1 - - [16/Jun/2014 16:44:43] "GET /bar HTTP/1.1" 200 12 0.0011
そして Chrome の cookie はこのようになっています。
rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFMzAwNzExMjliNTNhNGZkYTBhNWI2%0ANjQzMmZkOWUzZWVhYTFjYzEyNGIxYWEzMGI3ZTM1ZmZhYzQxZjA3ZTc2OEki%0ACWNzcmYGOwBGIiU5ZWFhMzk3YjM2ODliMGMwOWMyMGUzMzliOGY5NTYxOEki%0ADXRyYWNraW5nBjsARnsHSSIUSFRUUF9VU0VSX0FHRU5UBjsARiItOTYzN2Y4%0AZjQ2OThiYWNiOTZkODY4ZTZkMjFjNTc3NTU2NDEyMzAwMkkiGUhUVFBfQUND%0ARVBUX0xBTkdVQUdFBjsARiItMjY2OGNjYzBiMjZjMWFkYTFlNDUwYmYxM2Uy%0AMWU1MTUwMWE0MDQyMkkiDG1lc3NhZ2UGOwBGSSIRSGVsbG8gV29ybGQhBjsA%0ARg%3D%3D%0A--9cb3bf80af8ff28e027e9b59c7009f97cac215e6
では、Ruby - Rails で (Rack の) セッション情報を Cookie に保存する仕組み - Qiita に従って、この cookie の中身を確認してみます。
URI デコードと -- での切り出し
require 'base64' require 'uri' require 'openssl' session_in_cookie = "BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFMzAwNzExMjliNTNhNGZkYTBhNWI2%0ANjQzMmZkOWUzZWVhYTFjYzEyNGIxYWEzMGI3ZTM1ZmZhYzQxZjA3ZTc2OEki%0ACWNzcmYGOwBGIiU5ZWFhMzk3YjM2ODliMGMwOWMyMGUzMzliOGY5NTYxOEki%0ADXRyYWNraW5nBjsARnsHSSIUSFRUUF9VU0VSX0FHRU5UBjsARiItOTYzN2Y4%0AZjQ2OThiYWNiOTZkODY4ZTZkMjFjNTc3NTU2NDEyMzAwMkkiGUhUVFBfQUND%0ARVBUX0xBTkdVQUdFBjsARiItMjY2OGNjYzBiMjZjMWFkYTFlNDUwYmYxM2Uy%0AMWU1MTUwMWE0MDQyMkkiDG1lc3NhZ2UGOwBGSSIRSGVsbG8gV29ybGQhBjsA%0ARg%3D%3D%0A--9cb3bf80af8ff28e027e9b59c7009f97cac215e6" session_base64, digest = URI.decode(session_in_cookie).split("--") # => ["BAh7CUkiD3Nlc3Npb25faWQGOgZFRiJFMzAwNzExMjliNTNhNGZkYTBhNWI2\nNjQzMmZkOWUzZWVhYTFjYzEyNGIxYWEzMGI3ZTM1ZmZhYzQxZjA3ZTc2OEki\nCWNzcmYGOwBGIiU5ZWFhMzk3YjM2ODliMGMwOWMyMGUzMzliOGY5NTYxOEki\nDXRyYWNraW5nBjsARnsHSSIUSFRUUF9VU0VSX0FHRU5UBjsARiItOTYzN2Y4\nZjQ2OThiYWNiOTZkODY4ZTZkMjFjNTc3NTU2NDEyMzAwMkkiGUhUVFBfQUND\nRVBUX0xBTkdVQUdFBjsARiItMjY2OGNjYzBiMjZjMWFkYTFlNDUwYmYxM2Uy\nMWU1MTUwMWE0MDQyMkkiDG1lc3NhZ2UGOwBGSSIRSGVsbG8gV29ybGQhBjsA\nRg==\n", "9cb3bf80af8ff28e027e9b59c7009f97cac215e6"]
Base64 デコードして Marshal.load でオブジェクトを復元
Marshal.load(Base64.decode64(session_base64)) # => {"session_id"=> "30071129b53a4fda0a5b66432fd9e3eeaa1cc124b1aa30b7e35ffac41f07e768", "csrf"=>"9eaa397b3689b0c09c20e339b8f95618", "tracking"=> {"HTTP_USER_AGENT"=>"9637f8f4698bacb96d868e6d21c5775564123002", "HTTP_ACCEPT_LANGUAGE"=>"2668ccc0b26c1ada1e450bf13e21e51501a40422"}, "message"=>"Hello World!"}
ということで、ログと全く同じ情報を得ることが出来ました。
つまり、enable :sessions
つまり Rack::Session::Cookie を使った場合、cookie には、セッション情報も、session ハッシュの値も、全て含まれていることが分かりました。
秘密鍵 secret でハッシュ値が作られているか確認
上のコードでさらっと split しているが、Cookie に格納される情報は Marshal.dump -> Base64 エンコードした文字列のあと -- に続いて、ハッシュ値が書かれている。これは -- より前の文字列と秘密鍵を使って OpenSSL::HMAC.hexdigest で作成したもので、Cookie 生成時に付与され、受け取ったときに一致するか検証される。
このため、セッションの中身を書き換えたところで、秘密鍵を知らない限り、適切なハッシュ値を付与することができず、改ざんを防げるわけだ。
この秘密鍵は Rails アプリケーション作成時にランダムに生成される…
Ruby - Rails で (Rack の) セッション情報を Cookie に保存する仕組み - Qiita
今回は Rails じゃなく Sinatra ですし、秘密鍵は先ほどのスクリプトにも記載したように
set :sessions, secret: 'xxx'
です。これを使って cookie のハッシュを作ってみます。
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, "xxx", session_base64) # => "9cb3bf80af8ff28e027e9b59c7009f97cac215e6" OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, "xxx", session_base64) == digest # => true
等しいですね。ということで同じプロセスをたどっていることになります。
つまり、 :secret で Sinatra / Rack へ渡す値は秘密鍵として用いられること、そのためできるだけ長い方が好ましいことが分かります。
session (cookie)のデフォルト値
rack 1.5.2 の Rack::Session::Abstract::ID クラスには以下のように宣言されています。domainは nil ですね。
ついでにセッション ID についても載せておきます。
class ID DEFAULT_OPTIONS = { :key => 'rack.session', :path => '/', :domain => nil, :expire_after => nil, :secure => false, :httponly => true, :defer => false, :renew => false, :sidbits => 128, :cookie_only => true, :secure_random => (::SecureRandom rescue false) } attr_reader :key, :default_options def initialize(app, options={}) @app = app @default_options = self.class::DEFAULT_OPTIONS.merge(options) @key = @default_options.delete(:key) @cookie_only = @default_options.delete(:cookie_only) initialize_sid end #:(略) private def initialize_sid @sidbits = @default_options[:sidbits] @sid_secure = @default_options[:secure_random] @sid_length = @sidbits / 4 end # Generate a new session id using Ruby #rand. The size of the # session id is controlled by the :sidbits option. # Monkey patch this to use custom methods for session id generation. def generate_sid(secure = @sid_secure) if secure secure.hex(@sid_length) else "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1) end rescue NotImplementedError generate_sid(false) end #:(略) end
domain とか path とかを付けてトラブる
Sinatra 本家の FAQ に従って domain とか path とかを設定したら、なぜか動かない。
よくよく調べてみたら、domain の設定を間違っていました。コメントアウトしたら session ハッシュに入れた情報を cookie にセット出来ましたし、別ページでも取得出来ました。
ところで、インターネット上の大半のサイトが同様に domain を書いてるのは何でだろう。
そして、domain は設定しない方が良いというのを見つけて驚愕。
ので、domain とか path とかの設定は削除しておきましょう。
たまに誤解があるようですが、Cookieを設定する場合のDomain属性は *設定しない* のがもっとも安全です。
すなわち、Domain=example.comを指定したCookieは、www.example.comにも送信されます。Domain属性を指定しないCookieは、example.comに送信され、www.example.comには送信されません。
これは、CookieのRFC2965(旧規格)、RFC6265(現規格)には明確に記述されています。
CookieのDomain属性は *指定しない* が一番安全 | 徳丸浩の日記
IPA の文書に誤りがあるとの指摘も含まれており、なんだかガクブルです。
*1:ただしバージョンが 0.0.1 で 2012 年 2 月更新なので、動作するか要確認