Railsのセッションのしくみを徹底解説!Cookieとの違いと最適な保存先の選び方
もし、あなたのRailsアプリが「誰がアクセスしているのか」を一切判別できなかったらどうでしょう?同じ人が2つの異なるページをリクエストしたことすら分からず、保存したデータはレスポンスを返した瞬間に消えてしまう——そんな状態を想像してみてください。
ほぼ静的なサイトであれば、それでも問題ないかもしれません。しかし、ほとんどのアプリケーションでは、ユーザーに関する何らかのデータを保存する必要があります。ユーザーIDかもしれないし、言語設定、あるいは「iPadでも常にデスクトップ版サイトを表示したい」という好みかもしれません。
こうしたデータの保存先として最適なのがsessionです。複数のリクエストにまたがって保持しておきたい、小さなデータの断片たちです。
セッションの使い方はとてもシンプルです。
session[:current_user_id] = @user.id
しかし、その裏側には少し「魔法」のような仕組みが隠れています。そもそもセッションとは何なのか?Railsはどうやって適切なデータを適切な人に届けているのか?そして、セッションデータはどこに保存すればよいのでしょうか?
セッションとは何か?
セッションとは、あるリクエスト中に保存したデータを、後続のリクエストでも読み出せるようにするための仕組みです。
コントローラのアクション内でデータをセットできます。
def create
# ...
session[:current_user_id] = @user.id
# ...
end
別のアクションで読み出せます。
def index
current_user = User.find_by_id(session[:current_user_id])
# ...
end
一見地味な機能に見えるかもしれません。しかし、これが正しく動くためには、ユーザーのブラウザとRailsアプリの間の綿密な連携が必要です。そして、その鍵となるのがクッキー(Cookie)です。
クッキーの基本的なしくみ
Webページをリクエストすると、サーバーはレスポンスとともにクッキーを設定できます。
~ jweiss$ curl -I https://www.google.com | grep Set-Cookie
Set-Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly
ブラウザはこのクッキーを保存します。そして、クッキーが有効期限を迎えるまでの間、リクエストのたびにブラウザはクッキーをサーバーへ送り返します。
...
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: www.google.com
> Accept: */*
> Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly
...
多くのクッキーは、一見すると意味不明な文字列に見えます。それは意図的なものです。クッキーの中身はユーザー向けの情報ではないからです。クッキーが何を意味するのかを解釈するのは、Railsアプリの役割です。アプリ自身が設定したのですから、アプリ自身が読めるのは当然ですね。
クッキーとセッションの関係
ここまでの話を整理しましょう。クッキーがあり、あるリクエストでデータを入れれば、次のリクエストで同じデータが取り出せる。それならセッションと何が違うのでしょうか?
実は、Railsのデフォルト設定では、両者に大きな違いはありません。Railsはクッキーに対して署名などのセキュリティ処理を施しますが、それ以外は期待どおりに動作します。アプリがクッキーにデータを書き込み、同じデータがクッキーから取り出される。もしこれだけの話なら、セッションとクッキーを区別する必要すらないでしょう。
しかし、セッションデータの保存先としてクッキーが常に最適とは限りません。
クッキーに保存できるデータは約4KBまで。
通常は十分なサイズですが、足りなくなることもあります。
クッキーはすべてのリクエストに付与されて送信される。
大きなクッキーはリクエストとレスポンスを肥大化させ、サイト全体を遅くします。
secret_key_baseが漏洩すると、ユーザー側でクッキーの中身を改ざんできてしまう。そこに
current_user_idなどが含まれている場合、誰でも好きなユーザーになりすませてしまいます!誤った種類のデータをクッキーに入れると、セキュリティ上のリスクになる。
注意深く運用すれば、これらは大きな問題にはなりません。
しかし、何らかの理由でクッキーにセッションデータを保存できない場合、Railsには他の保存先が用意されています。
代替セッションストアのしくみ
クッキーストア以外のセッションストアは、いずれもほぼ同じ仕組みで動作します。具体的な例で見たほうが理解しやすいでしょう。
ActiveRecordでセッションを管理している場合を考えてみます。
アプリ内で
session[:current_user_id] = 1を呼び出し、まだセッションが存在しないとき:RailsはランダムなセッションID(たとえば
09497d46978bf6f32265fefb5cc52264)を持つ新しいレコードをsessionsテーブルに作成します。{current_user_id: 1}(Base64エンコード済み)をそのレコードのdata属性に保存します。生成されたセッションID
09497d46978bf6f32265fefb5cc52264を、Set-Cookieヘッダ経由でブラウザに返します。
次にページをリクエストするときは:
ブラウザは
Cookie:ヘッダを使って、同じクッキーをアプリへ送信します。(例:
Cookie: _my_app_session=09497d46978bf6f32265fefb5cc52264;path=/; HttpOnly)session[:current_user_id]を呼び出すと:アプリはクッキーからセッションIDを取り出し、
sessionsテーブルから該当レコードを検索します。そして、そのレコードの
data属性からcurrent_user_idを返します。
データベースであれ、Memcachedであれ、Redisであれ、その他の場所であれ、基本的な流れは同じです。クッキーにはセッションIDだけが入っており、RailsアプリはそのIDを使ってセッションストアから実データを引き出します。
クッキーストア、キャッシュストア、データベースストア、どれを選ぶべきか
要件を満たせるのであれば、クッキーへのセッション保存が断然おすすめです。追加のインフラもセットアップも不要だからです。
ただし、クッキーセッションストアの限界を超える必要が出てきた場合は、選択肢が2つあります。データベースに保存するか、キャッシュに保存するかです。
キャッシュに保存する
パーシャルやデータのキャッシュに、すでにMemcachedなどを使っているかもしれません。その場合、キャッシュストアは2番目に手軽なセッション保存先です。すでに環境が整っているからです。
キャッシュが肥大化しても古いセッションは自動的に追い出されるため、セッションストアの管理を心配する必要がありません。また、キャッシュは多くの場合メモリ上に置かれるため、高速です。
ただし、完璧ではありません。
古いセッションを本当に保持したいなら、キャッシュから追い出されるのは望ましくありません。
セッションと通常のキャッシュデータが容量を奪い合います。メモリが不足すると、大量のキャッシュミスや早期失効したセッションに悩まされる可能性があります。
キャッシュのリセットが必要になったとき(Railsのアップグレードで古いキャッシュが不正確になった場合など)、全員のセッションを失効させるしか方法がありません。
それでも、Avvoではこの方法でセッションデータを保存しており、今のところうまく機能しています。
データベースに保存する
正当な有効期限が来るまでセッションデータを確実に保持したいなら、データベースへの保存が向いています。Redisでも、ActiveRecordでも、その他のストアでも構いません。
ただし、データベース保存にも欠点があります。
ストアによっては、セッションが自動的にクリーンアップされない。
期限切れセッションを自分で削除する仕組みを用意する必要があります。
セッションデータで満杯になったときのデータベースの挙動を把握しておく必要がある。
Redisをセッションストアに使う場合、すべてのセッションデータをメモリに保持しようとするでしょうか?サーバーのメモリは足りていますか?スワップが激しすぎて
sshでログインして修復することすらできない、なんて事態にはなりたくないですね。セッションデータの生成タイミングに注意しないと、無駄なセッションでデータベースが埋め尽くされる。
たとえば、うっかり毎リクエストでセッションに触ってしまうと、Googlebotが数十万件の無意味なセッションを生成しかねません。それは大変なことになります。
こうした問題は実際にはあまり起こりません。それでも、頭の片隅に置いておく価値はあります。
結局、どのセッションストアを選ぶべきか
クッキーストアの制限に引っかからない自信があるなら、迷わずクッキーストアを使いましょう。セットアップの手間も少なく、保守も楽です。
キャッシュとデータベースのどちらを選ぶかは、「セッションが予定より早く失効したらどれほど困るか」という判断基準によります。私はセッションデータをかなり一時的なものと捉えているので、キャッシュストアがよく合います。つまり、まずクッキーを試し、次にキャッシュ、最後にデータベースという順番で検討するのがおすすめです。
あなたのプロジェクトでは、セッションをどこに保存していますか?ぜひコメントで教えてください!
RubyやRailsの内部構造についてさらに学びたい方は、「gemのしくみ」に関する記事も参考にしてください。Webの仕組みそのものに興味があるなら、「Webサーバーとアプリケーションサーバーの違い」を解説した記事もおすすめです。
-
データバックアップの正しい方法とは?3-2-1ルールで大切なデータを守る
現代のIT社会において、包括的なバックアップ戦略を持つことは不可欠です。データが失われる原因は数多く存在し、バックアップを適切に行う方法を理解することは、深刻な事態を回避するために極めて重要です。では、具体的にどのようにデータをバックアップすればよいのでしょうか?データ損失のリスクサイバー攻撃、内部犯行、自然災害、記録メディアの破損、人的ミスなど、データを失う要因は枚挙にいとまがありません。個人にとってデータ損失は煩わしく心を痛める出来事ですが、企業にとっては取り返しのつかない結果をもたらしかねません。Consoltechによる以下の衝撃的な統計をご覧ください。重大なデータ損失を経験した企業の
-
Androidのデータバインディング入門:Data Binding Libraryでレイアウトとデータを結びつける方法
データバインディングとは、アプリが扱う「データ」と、画面上の視覚的なUI要素を結びつける(バインドする)ための手法です。この仕組みを使うと、UI側の値が更新されるたびに、裏側で保持しているデータも自動的に更新されます。 決して目新しい概念ではなく、AngularJS、React、Vueなど、多くのフロントエンドフレームワークがすでにこの仕組みを設計に取り入れています。 しかし本記事で注目するのはフロントエンドフレームワークではなく、モバイル開発です。GoogleはAndroid向けにData Binding Libraryを提供しており、これはAndroid Jetpackの一部として位置づけ