わし「クッキーとかセッションとか記憶トークンとか意味ワカンネ( ᵕ̩̩ㅅᵕ̩̩ )」
Railsチュートリアル8,9章が急に本気出してきたので改めてセッションやらクッキーについてまとめておきます…
ログイン機能を実践的に書くとかはしないのでよろしくな!
##セッションとクッキーの違い
####HTTP通信はステートレス
というのは恐らくどんな教材にも載ってますよね。
HTTP通信は過去にした通信をすべて忘れてしまいます。
あなたがログインフォームに打ち込んだメールアドレスやパスワードも、本来なら次のページに移る頃には無かったことになってしまうのです。
ではどのようにしてログイン情報を保存しているのでしょうか。
####クッキーは保管庫・セッションは接続
HTTP通信はステートレスですが、ブラウザにはしっかりと情報を保存させておく場所が用意されております。
それが**クッキー(cookies)です。
ブラウザのクッキーに、ログイン情報(ユーザーIDなど)を格納させておくことで、
クライアントとサーバーサイド間でクッキーの情報を照合し、ログインしているのかどうかを確認することができます。
このクライアントとサーバーの接続関係をセッション(session)**と呼びます。
そしてこのセッションを確立するためにRailsにはsessionメソッドがあります。
##sessionメソッド
session[:キー] = 値
とすることで値を自動的にハッシュ化し、ブラウザに一時クッキーを渡すことが出来ます。
session[:user_id] = user.id
例えば上記のようにユーザーのIDをクッキーへ渡しておき、
User.find_by(id: session[:user_id])
等でログインユーザーのインスタンスを取得することで
ページ遷移後もログインユーザー情報はそのままにあれこれすることができます。
####よくあるログイン機能の一例
ログインフォームを例にすると、Railsでのセッションの確立は以下のような流れになっています。
-
ユーザーがメールアドレス・パスワードをフォームに打ち込む
-
メールアドレスでDBからユーザーを検索する
(user = User.find_by(email: params[:session][:email].downcase)
) -
パスワードを検証する
(user.authenticate(params[:session][:password])
) -
ユーザーが存在し、パスワードも検証できているか確認
(if user && user.authenticate(params[:session][:password])
) -
上記がtrueを返したとき、ブラウザにログインユーザーIDのクッキーを渡す
(session[:user_id] = user.id
) -
@current_user等のインスタンス変数にログインユーザーを格納
(@current_user ||= User.find_by(id: session[:user_id])
)※ -
ログアウトボタンでセッションを断つ
(session[:user_id] = nil
でクッキーをnilにするとか)
※@current_user = @current_user || User.find_by(id: session[:user_id])
と同じ。
@current_user
がすでに存在していれば@current_user
を返し、
無ければUser.find_by(id: session[:user_id])
を返してくれる。
※find_byを使用することでユーザーが見つからなかった場合にnilを返すようにしている。
findメソッドだと例外を発生させてしまう。
「ユーザーが未ログイン」という状態でも例外が発生しないよう、find_byメソッドを使用する。
ただし、sessionメソッドで渡すことができるクッキーは一時クッキーであり、**ブラウザを閉じた瞬間セッションは失われてしまいます**。 ブラウザを閉じてもセッションを継続させ、ユーザーが故意にログアウトした時に初めてセッションを失わせる方法はないのでしょうか。
あります。cookiesメソッドです。
##cookiesメソッド
cookieメソッドは文字通り、ブラウザにクッキーを渡すことができます。
sessionメソッドと違い、オプションを渡すことで永続的なクッキーにすることができ、
ブラウザが閉じられてもセッションは継続されることになります。
ただし、cookieメソッドでは自動的にハッシュ化されないという問題もあるので対策を講じる必要があります。
(もちろん、通信をSSL化させる等の根本的な対策はある前提で!)
cookie[:キー] = 値 cookies.permanent[:キー] = 値 cookies.signed[:キー] = 値
cookies.permanent
メソッドはクッキーを20年後に期限切れするよう設定することができます。
こうすることで実質的な永続クッキーを設定することができます。
※本来cookiesメソッドは以下のようにvalue(値)とexpires(期間)をハッシュで渡しますが、同内容のpermanentメソッドとしてRailsが用意してくれています。
cookies[:キー] = { value: 値, expires: 20.years.from_now.utc }
cookies.signed
メソッドは値を暗号化させます。
同じようにcookies[:キー].signedで暗号化される前の値を取り出すことができます。
メソッドチェーンも使用するとこんな感じ
cookies.permanent.signed[:user_id] = user.id
こうすることで、ユーザー情報を暗号化し永続クッキーへ保存することが出来ます。
User.find_by(id: cookies.signed[:user_id])
でインスタンスも取得できます。
しかし、仮に第三者に暗号化クッキーが漏れてしまった場合、永続クッキーを使って悪さをされてしまいます。
そこで、ユーザーIDに加え、クッキーにユーザーに紐付いた暗号が存在していなければログインできないようにします。
具体的には以下のような流れです。
・ユーザーがログインする
・システムが暗号を作成する
・システムが暗号をユーザーインスタンスとクッキーに渡す
・ブラウザを閉じてから再度アクセスする
・クッキーに保存された暗号がユーザーインスタンスに保存された暗号と一致するか確認
・一致すればログイン成功
・ユーザーがログアウトする
・ユーザーインスタンスの暗号がnilになる
・第三者がクッキーを取得し悪いことしようとする
・ユーザーインスタンスの暗号がnilになっているためログインできない
・めでたしめでたし
暗号はログイン毎に違うものが作成されるため、仮にログイン中に第三者にクッキーが漏洩したとしても、ログアウトしてしまえば暗号が再発行されるので安心です。
そしてこの暗号のことを記憶トークンと呼びます。
Railsチュートリアル中に死ぬほど見た単語ですが、カタカナが多くて理解に苦しんだのであえて暗号と書いてました。
※パスワードは人の手で作る暗号で、トークンはシステム側で作り出される暗号と覚えておきましょう。
以下、cookieメソッドを使用した実例です。
※userモデルにremember_digestカラムと、
models/user.rbに**attr_accessor :remember_token
**を用意しておきましょう
あと色々実装必要ですが、この場ではざっくりとした流れの説明に留めます。詳しくはRailsチュートリアル9章へ急げ!
####永続クッキーの実装例
ユーザーがメールアドレス・パスワードをフォームに打ち込み確認されるまでは一緒
- 記憶トークンを作成するメソッドを用意
※オブジェクトのインスタンスが必要ない場合はクラスメソッドで定義するのが常道らしい
def User.new_token
SecureRandom.urlsafe_base64
end
- 記憶トークンをユーザーインスタンスのremember_token属性へ代入
self.remember_token = User.new_token
- 記憶トークンを暗号化させるメソッドを用意
生のパスワードをDBに保存しないのと同様に、記憶トークンも暗号化させてインスタンスへ保存させます。
暗号化する方法はパスワードを実装した際に追加したhas_secure_passwordのソースコードが参考になります。
具体的にはRails gemのsecure_password.rb内の以下の部分にて確認できます。
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
以上を参考に、引数stringに記憶トークンを渡して暗号化させるメソッドを定義しましょう。
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
- remember_digestカラムに暗号化された記憶トークンを保存
※has_secure_passwordの認証を回避するためupdate_attributeメソッドを使用しましょう。
update_attribute(:remember_digest, User.digest(self.remember_token))
- cookiesメソッドを使用して記憶トークンとユーザーIDをクッキーへ渡す
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
- クッキー内の記憶トークンとユーザーインスタンスの暗号化記憶トークンが一致するかを確認するメソッドを用意
has_secure_passwordではauthenticateメソッドが提供され、生のパスワードとの照合をすることができました。
先程と同様にソースコードを参考にしながら実装します。
def authenticated?(remember_token)
BCrypt::Password.new(self.remember_digest).is_password?(remember_token)
end
- クッキーを照合し、ユーザーと記憶トークンの照合を行う
user = User.find_by(id: cookies.signed[:user_id])
if user && user.authenticated?(cookies[:remember_token])
-
上記がtrueを返した場合、@current_user等のインスタンス変数にログインユーザーを格納
-
ログアウト時にクッキーを削除し、:remember_digestカラムをnilへ変更する
cookies.delete(:user_id)
cookies.delete(:remember_token)
update_attribute(:remember_digest, nil)
以上!!!
##おわり
永続クッキーを用いることでユーザーに優しいログイン機能を作ることができます。
しかしながら、クッキーを残しておくということはセキュリティに問題も残ることを覚えておきましょう俺。
Railsチュートリアルは8,9賞からが本番な感じがするので頑張って乗り切りましょう…