いまだにsessionとかcookieがよくわからん話
セッションとかcookieがよくわからんので理解するために何かする。
とりあえず手元でRails環境作って、
Rails5の話なので諸々注意。
bcrypt...パスワードのハッシュ化に使われるアルゴリズム
Ruby実装
ハッシュ化されたパスワードは、暗号化されたパスワードとよく誤解されがちです。例えば、(実は本書の第1版や第2版でも間違っていたのですが) has_secure_passwordのソースコードでもこの手の間違いがあります。というのも、専門用語としての「暗号」というのは、設計上元に戻すことができることを指します (暗号化できるという文には、復号もできるというニュアンスが含まれます)。一方、「パスワードのハッシュ化」では元に戻せない (不可逆) という点が重要になります。したがって、「計算量的に元のパスワードを復元するのは困難である」という点を強調するために、暗号化ではなくハッシュ化という用語を使っています。
ほーなるほど...確かにハッシュ化と暗号化は違うという話をどこかで聞いた気がするがそういうことだったのか。
has_secure_password
内部的には InstanceMethodsOnActivation
で xxx =
メソッドにおいて、 xxx_digit に bcyrptでハッシュ化したパスワードを入れるようにしているっぽい
define_method("#{attribute}=") do |unencrypted_password|
if unencrypted_password.nil?
instance_variable_set("@#{attribute}", nil)
self.public_send("#{attribute}_digest=", nil)
elsif !unencrypted_password.empty?
instance_variable_set("@#{attribute}", unencrypted_password)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
つまりチュートリアルの以下かな
セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる
ちょっと納得
全然進んでないが一旦区切り。
has_secure_passwordの中身を見れたことが少し収穫
ActiveModel::ForbiddenAttributesError in UsersController#create
Strong Parameter使ってないとエラー出るのか。出るんだっけ?なんか知らなかった。
でもRails5のドキュメントの段階から書いてあるならただ知らなかっただけだな。
metal.rbはActionController固有の基本クラスっぽい
今更だけど...
def []=(k, v)
k = k.to_s
@discard.delete k
@flashes[k] = v
end
が
test[:some] = 'test'
と使えるのは、rubyの糖衣構文。
ドキュメント上は以下で定義されている。
まぁとにかく
flash[:success] = "Welcome to the Sample App!"
とやると、
class Flash
class FlashHash
def []=(k, v)
k = k.to_s
@discard.delete k
@flashes[k] = v
end
end
end
ここで@flashesに入るわけだ。
でもリクエスト跨いだらflashesの中身消えませんか?
あーなんかコミットってあったなそういえば。
Flash::RequestMethod#commit_flash
で
session["flash"] = flash_hash.to_session_value
的な感じでsessionにflashの内容を登録しているのか。
いい感じに sessionってなんですか?というところにたどり着けたわ。
(railsにおけるsession = ユーザー毎の情報を格納するための仕組み。雰囲気で使ってたな...)
それは良いとして、どこでクリアしてるんだ?多分discard周りだと思うけど。
あーdiscardはfrom_session_valueのnewで設定されるのか
def self.from_session_value(value) # :nodoc:
case value
when FlashHash # Rails 3.1, 3.2
...
when Hash # Rails 4.0
flashes = value["flashes"]
if discard = value["discard"]
flashes.except!(*discard)
end
new(flashes, flashes.keys)
else
new
end
end
...
def initialize(flashes = {}, discard = []) # :nodoc:
@discard = Set.new(stringify_array(discard))
@flashes = flashes.stringify_keys
@now = nil
end
でそのリクエストのcommit_flashで session["flash"] = flash_hash.to_session_value によってsession内のflashが削除されるのかなるほど。
def commit_flash # :nodoc:
...
if flash_hash && (flash_hash.present? || session.key?("flash"))
session["flash"] = flash_hash.to_session_value
self.flash = flash_hash.dup
end
end
def to_session_value # :nodoc:
flashes_to_keep = @flashes.except(*@discard)
return nil if flashes_to_keep.empty?
{ "discard" => [], "flashes" => flashes_to_keep }
end
ずっと見落としてたんですけどかなしい
そうだ元々Railsチュートリアルの流れにのってたんだった。
戻ろうか、もう少しsessionを深堀しようか...
戻ろう
とりあえずこっち
ここでいうログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))です。
sessionとかcookieとかってよく使われるのはここですよね。
なんかどうしてもネットワークのセッションが頭をよぎるんだ...
sessionについてググっていたら出てきた話。
実体としては
のdef load!
id, session = @by.load_session @req
options[:id] = id
@delegate.replace(stringify_keys(session))
@loaded = true
end
この辺りに辿り着きそうな雰囲気
こういうことするにはzennのスクラップが一番良いな
ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。セッションはHTTPプロトコルと階層が異なる (上の階層にある) ので、HTTPの特性とは別に (若干影響は受けるものの) 接続を確保できます。
概念的にはこれが全てか。WebアプリケーションにおけるセッションというのはステートレスなHTTP上でユーザー識別等のステートを保持しておくための仕組みという話。
Railsにおけるsessionはその実現のための仕組みと。cookieはその実現のためによく使われる手段の1つ的な位置付けかな...やっぱり同階層の話じゃないよね。
Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。cookiesは、あるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できます。
まぁそうですよね。1度やったけど当時は理解の解像度が低かった的な感じなんかな。なんかこの辺ぼんやりとしてたな...
セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと統一的に理解できて便利です
sessions_controllerってまぁよくあるけどなんも考えてなかったわ。sessionをRESTfulなリソースとして扱ってるってことか
ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった具合です。
これセッションについての具体的なイメージがなかったから頭に入っていなかったな。いや今も明確には理解してないぞ。
一旦 リスト 8.4:ログインフォームのコード まで。
railsのform周り(form_withとか)のこともあんまり理解してない。
というかform系全般理解があやふや
なんかflash周りの表示、turbo-railsを取り除かないとうまく表示できないっぽい。なんかあったなこんなの...
sessionメソッドで作成した一時cookiesは自動的に暗号化され、リスト 8.14のコードは保護されます。そしてここが重要なのですが、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできないのです。ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当しません。cookiesメソッドで作成した「永続的セッション」ではそこまで断言はできません。永続的なcookiesには、セッションハイジャックという攻撃を受ける可能性が常につきまといます。ユーザーのブラウザ上に保存される情報については、第9章でもう少し注意深く扱うことにします。
この辺はシンプルに分からんが、どちらも(sessionは初期設定通り)cookieを使っている想定ならcookieの設定で実現してるとかだろうか?
cookiesのコードリーディング記事。これをお供にコード見てみればわかりそうやな。
ちょっと整理するか。
Session
Webアプリケーションにおいて、ステートレスなHTTP上で主にユーザーに関する状態を保持するための仕組み。ユーザーがログインしているかどうかや、ユーザーの一時的なデータを保持するために使用される。
Railsでは、この目的のためにsessionメソッドが提供されている。sessionのデフォルトの実装はcookieを使用するが、設定に応じて他のストレージに変更することも可能。
おうモロあるじゃん
cookieもあるやん
なぜこれを最初に見なかったのか。過去目は通したはずだが完全に抜けてたな。
- CS分野におけるセッションは「継続的な通信」もしくはそれを実現するための仕組み、のこと
- 継続的な通信にはそれを管理するためのなんらかの状態(ステート)が必要
- ステートフルなプロトコルを用いた通信では、プロトコル自体に継続的な通信を確立するための仕組みが入っている
- ステートレスなプロトコルには継続的な通信を確立するための仕組みは無いので、何かしらの仕組みを追加で導入する必要がある。
- 往々にしてこの仕組み自体もセッションと呼ばれる
- cookieはステートレスなhttpにセッションを導入するための主要な手段の1つ
まぁまぁしっくりくる表現。ひとまずこれを土台にして良いかな。
ChatGPTって、しっくりくる表現を探すための手段として便利だな...
馬鹿になってる気もするけど、今までググったり本読んだりで探していたものを代替していると思えば実態としては変わらないかもしれない。
結局のところ知ってる、扱える範囲の言葉しか拾えないだろうし。
じゃあ改めてこれ
Railsアプリケーションは、ユーザーごとにセッションを設定します。
この場合のセッションは
- セッションを保持するための仕組み
- セッションストア(上に包含されるか?)
という感じだろうか。
言い換えれば継続的な通信を確立するための仕組みをユーザ毎に設定するという感じ?いや...継続的な通信を確立するための仕組みがもともとあって、それをユーザー毎にセットアップするぜという感じ?
セッション(という仕組みを使って)ユーザーを識別できるようにします
の方があってるかな...?
セッション(という仕組みを使って) ユーザー毎に状態をもたせられるようにします、でも良い気もするが...
総称して「セッションを設定します」なのかね。
前のリクエストの情報を次のリクエストでも利用するために
これは前のリクエストと次のリクエストを継続的な通信として扱う、
ということと大体同じ意味になるかな。
Railsアプリケーションは、ユーザーごとにセッションを設定します。
でもやっぱこれわかんねぇな...イメージできない。何してんの?
コードみるか...
このprepare_sessionで Request::Session.create(self, req, @default_options)
してるっぽいが、 prepare_sessionは誰が呼んでるんだという
↓
rackが何かやってるっぽい...???
↓
分からん
controllerからのセッションの参照は
より、requestからやっている様子。ってことはrequestが作られる過程で、sessionも作られるんですかね👀
sessionの参照はここで行われている様子
load_for_read!
を辿ると
にたどり着く。
id, session = @by.load_session @req
でセッションを読み込んでいる雰囲気
@byはなんですか?と。
よりActionDispatch::Request::Session.create
で設定されている様子。
このcreateはActionDispatch::Session::SessionObject#prepare_session
で呼び出されている
selfを見てみると...
(rdbg) list 75 # command
75| def prepare_session(req)
=> 76| debugger
77| Request::Session.create(self, req, @default_options)
78| end
79|
80| def loaded_session?(session)
81| !session.is_a?(Request::Session) || session.loaded?
82| end
83| end
84|
(ruby) self.class
ActionDispatch::Session::CookieStore
なので改めて
は、
ActionDispatch::Session::CookieStore
の load_sessionを呼び出していることになる。
ActionDispatch::Session::CookieStore の load_session周りは以下の感じ
unpacked_cookie_data
は get_cookie(req)
で cookieを読み出して、headerにセットしている
request.cookie_jar.signed_or_encrypted
では基本暗号化されたcookie_jar(cookieの入れ物的な概念)を返す。
cookie_jar(req)
には EncryptedKeyRotatingCookieJar のインスタンスが入っており、
[@key] は以下の形で parseされた(おそらく複合された) cookieの中身が参照できる
ということで参照周りはなんとなくイメージがついた。
callerで見てみる
(ruby) puts caller
.../gems/debug-1.9.2/lib/debug/thread_client.rb:415:in `eval'
(中略)
.../gems/debug-1.9.2/lib/debug/session.rb:2641:in `debugger'
.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/session/abstract_store.rb:76:in `prepare_session'
.../gems/rack-session-2.0.0/lib/rack/session/abstract/id.rb:271:in `context'
.../gems/rack-session-2.0.0/lib/rack/session/abstract/id.rb:266:in `call'
.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/cookies.rb:689:in `call'
(中略)
.../gems/3.3.0/gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/callbacks.rb:28:in `call'
.../gems/3.3.0/gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/executor.rb:14:in `call'
(中略)
.../gems/3.3.0/gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/request_id.rb:28:in `call'
(中略)
.../gems/3.3.0/gems/puma-6.4.2/lib/puma/request.rb:99:in `handle_request'
(中略)
.../gems/3.3.0/gems/puma-6.4.2/lib/puma/thread_pool.rb:155:in `block in spawn_thread'
細かいことは分からんが、
リクエストがあったタイミングで、rack-session-2.0.0/lib/rack/session/abstract/id.rb:271 で呼び出されている様子。
まぁそもそも middleware/session/abstract_store.rb
自体middlewareと言ってるんだし、rackの領域の話なんだろうな。
ということで次は rack-session
.../gems/rack-session-2.0.0/lib/rack/session/abstract/id.rb:271:in `context'
の該当箇所は以下。
が、すぐ下に prepare_session が定義されている
stack_traceは
.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/session/abstract_store.rb:76:in `prepare_session'
って言ってるんで継承が関連してくるんだろう。が、こいつは Persisted クラスのインスタンスメソッドなので include されてる系ではないな...
あと
がとても気になるんだが、idベースの sessioning service のフレームワークってなんぞ?
このコメントが説明しているのは、Rack::Session::Abstract::IDというモジュールやクラスが提供する「IDベースのセッション管理フレームワーク」についてです。このフレームワークは、セッション管理をIDを使って行うための基本的な枠組みを提供します。
具体的には、セッションを維持するために、クライアントに送られるクッキーにはセッションデータ自体ではなく、セッションを識別するための「ID」が保存されます。そして、サーバー側ではそのIDをもとに、対応するセッションデータを管理します。
ほーんなるほど...?
これかな。
あらゆるセッションは、cookieを利用してセッション固有のIDを保存します(cookieは必ず使うこと: セッションIDをURLで渡すとセキュリティが低下するため、この方法はRailsで許可されません)。
ほとんどのセッションストアでは、サーバー上のセッションデータ(データベーステーブルなど)を検索するときにこのIDを使います。
CookieStoreは、Railsで推奨されているデフォルトのセッションストアであり、例外的にすべてのセッションデータをcookie自身に保存します(必要に応じてセッションIDも利用可能です)。CookieStoreには非常に軽量であるというメリットがあり、新規Webアプリケーションでセッションを利用するための準備も不要です。このcookieデータは改ざん防止のために暗号署名が追加されており、cookie自身も暗号化されているので、他人が読むことはできません(改ざんされたcookieはRailsに拒否されます)。
Rack::Session::Abstract::ID
は IDベースといいつつ、デフォルトのCookieStoreを使った場合には 全てのセッションデータを cookie自身に保存するのか。
確かに cookieをdecryptしたやつは中にuser_idとかも入ってるもんな...
ベースの部分ではより汎用的な Rack::Session::Abstract::ID
を提供するけど、デフォルトでは CookieStore使ってねという感じか。CoCを感じますなぁ...
stack_traceは
.../gems/actionpack-7.1.3.4/lib/action_dispatch/middleware/session/abstract_store.rb:76:in `prepare_session'
って言ってるんで継承が関連してくるんだろう。が、こいつは Persisted クラスのインスタンスメソッドなので include されてる系ではないな...
これの続き。
まぁこの通りか。
ActionDispatch::Session::CookieStore
は Rack::Session::Abstract::PersistedSecure
を継承していると。だからおそらく ActionDispatch::Session::CookieStore
が callされて、 context メソッド内で prepare_sessionが呼ばれた際に、lib/action_dispatch/middleware/session/cookie_store.rb の prepare_sessionが呼ばれるということだろうな。
整理すると...
前提
- CS分野でのセッション = 「継続的な通信 及びそれを実現するための仕組み」
- Web分野でのセッション = 「ステートレスなhttp上での継続的な通信 及びそれを実現するための仕組み」
- 基本的なユースケースとしては「ユーザーの状態を維持すること」が挙げられる。(ログイン状態を維持したり、ショッピングサイトにおけるカートの中身を維持したりなど)
Ruby on Railsにおけるセッション
概要
- Railsアプリケーションは、ユーザーごとにセッションを設定する
- Railsガイドより
- 「ユーザーごとにセッションを設定する」とは...
- サーバーリクエストに対してセッションIDを発行する
- そのセッションID毎にユーザーを識別する
実装
おおよそ以下3パートに分かれる
- セッションの初期設定
- セッションストアの選択。デフォルトでは
CookieStore
-
Rails.application.config.session_store :cookie_store
的なやつ
- セッションストアの選択。デフォルトでは
- サーバーがリクエストを受け取った際、Rack Middleware の階層で req(多分ActionDispatch::Request のインスタンス)のheaderに、ActionDispatch::Request::Sessionのインスタンスを設定する。その中には前項で設定したセッションストア も格納されている
- ちょっと怪しい...
- アプリがsessionを参照した場合
- ここで初めてセッションがloadされる(
@by.load_session @req
) - CookieStoreの場合はこの段階でリクエストヘッダーからcookieを読みに行く形(多分)
- ここで初めてセッションがloadされる(
改めて整理
SessionStore
を設定
1. Railsアプリ立ち上げ時、利用する-
Rails.application.config.session_store :cache_store
こういうやつ - おそらくこの設定によって2.が起きるようになると思われる
2. リクエスト受け取り時、Sessionインスタンスを作成
- 端的には
- Rackで
ActionDispatch::Session::CookieStore#call
によって、セッション管理用のインスタンスが作成されている
- Rackで
- 詳細としては...
-
Rack::Session::Abstract::Persisted#context
- 実体としては
ActionDispatch::Session::CookieStore
。CookieStore
がPersisted
を継承している
- 実体としては
-
ActionDispatch::Session::SessionObject#prepare_session
でActionDispatch::Request
インスタンスの@envに、ActionDispatch::Session
インスタンス(ActionDispatch::Session::CookieStore
のインスタンスを内包する)を設定する。- その際のkeyは
Rack::RACK_SESSION
- その際のkeyは
-
3. session 参照時、2.で作成したsessionインスタンスからセッションデータをロード
- 端的には...
-
- で登録した session内の session store の load_session メソッドを用いて、sessionデータを読み出す
-
- 詳細は...
- controller内で呼ばれるsessionメソッド
-
ActionDispatch::Metal
のdelegate :session, to: "@_request"
->ActionDispatch::Request
のinclude Rack::Request::Helpers#session
-
ActionDispatch::Request::Session
を取得する
-
-
session[:key]
はActionDispatch::Request::Session#[]
で実装される -
load_for_read!
->load!
の@by.load_session @req
でセッションがリードされる。@byの中には2.で設定した SessionStoreのインスタンスが入っている。デフォルトではCookieなので、ActionDispatch::Session::CookieStore
のインスタンスになる -
ActionDispatch::Session::CookieStore#load_session
で、cookieの中身を複合化して session_id と セッションデータのセットを返す
「header」という表現がいまいち
で記事作成中
Rackとかの説明を以下に書いたが、執筆中の記事で使わなくなった。勿体無いのでここに残す。
関連機能(クラス/モジュール)
今回参照する主な関連機能(クラス/モジュール)は以下の通り
- ActionDispatch
- ActionDispatch::Request
- ActionDispatch::Session
- ActionDispatch::Middleware::Session::CookieStore
- Rack
- Rack::Request
- Rack::Session
- Rack::Session::Abstract::SessionHash
- Rack::Session::Abstract::Persisted
補足説明
上に挙げたActionDispatch/Rack/Rack::Sessionについて本記事に関連する範囲の概要を記載する。
これらに関してイメージがつく方はこの項目は読み飛ばしてもらって構わない。
ActionDispatch
ActionDispatchは、Railsのリクエスト処理に関する機能群(クラス/モジュール)であり、セッション管理やエラーハンドリング、ルーティングなどを行うクラスを含んでいる。
これらはRailsのミドルウェアスタックで動作するものもあれば、アプリケーション上で使用される機能やデータ(例えばルーティング機能自体やコントローラ内で参照可能なリクエストに関するデータ)を提供するものもある。
この記事では主にセッションに関連する、ミドルウェア、データや機能について触れる。
Rack
RackはWebサーバーとアプリケーションの間のインタフェースを提供し、HTTPリクエスト処理を抽象化している。Rails等のWebフレームワークは、Rackを用いることでWebサーバーからのHTTPリクエストを標準化された方法で受け取り、処理できるようになる。
この記事では主に、Rackの環境変数の情報を操作・取得するためのヘルパーメソッド群について触れる。
Rack::Session
Rack::SessionはRackアプリケーション(この場合はRailsアプリケーション)にセッション機能を追加するためのミドルウェアで、セッション管理のための基本的な機能を提供する。Railsが定義するセッションストアはここで提供されているものをベースにして作成される。
この記事では主に、リクエストを受け取った際にセッションが用意される処理の流れ、およびセッションが参照される際の処理の流れについて触れる。
まだこの記事を書いてるわけだが...
補足として以下は加えたい
- source_locationとかbacktraceとかは使っている
- rubyでxxx[:key] とかは、xxxの[]メソッド&引数(key)で表される(rubyの公式ドキュメント)
この話1ヶ月くらいやってんな。
記事にすると倍くらいかかるという話。
まぁ趣味だから...と心の底から思えるようになりたいね。ちょっとコスパを気にしている節がある。