こんにちは。
Railsのgem "devise"を使って認証機能を実装すると、「ログイン状態の保持」機能を追加できますが、そもそも「ログイン」や「ログイン状態の保持」って何がどう管理されているのか気になったので調べてみました。
※記事の内容に誤りがありましたら、コメントでご指摘いただけますと幸いです。
開発環境
OS:windows11
Rails:7.2.2
devise:4.9.4
簡単なまとめ
最初にこの記事の内容を簡単にまとめておきます。
- HTTP がステートレスであるため、Railsはセッションを使ってクライアントの状態を管理している
- セッションの内容は暗号化されてCookieとして保存し、リクエストごとにRailsへ送り返されることで状態が復元される
- ユーザーがログインすると、セッションに user_id が保存され、「誰が操作しているか」を特定できるようになる
- 「ログイン状態の保持」をオンにすると、deviseがトークンを生成し、セッションとは別のCookieを作成する
- このトークンが有効な間は自動でログイン処理が行われる
詳細はこのあとに続きます。
Railsにおける状態管理
Railsでは、HTTPというプロトコルによって通信が行われますが、HTTPは「ステートレス」なプロトコルです。
「ステートレス」は、1回のリクエストとレスポンスのやり取りが行われたときの状態が保存されません。
そのため、同じクライアント(ブラウザ)が、続けて2回目のリクエストを送ったとしても、「先ほどと同じクライアント」を、Railsは認識できず、新たなクライアントからのリクエストとして扱います。
それでは困るので、Railsには状態を管理する機能が備わっており、それが 「セッション」 です。
Railsは、クライアントごとにセッションオブジェクトを用意し、そこにクライアントの状態を保存していくことで、個々の状態を管理します。
例えば、ショッピングサイトでカートにアイテムを追加すると、session[:item]に情報が保存されます。(セッションはハッシュ型なので、キーとバリューで保存します)
そして、Railsは、そのセッションを暗号化して、Cookieにしてクライアントに返し、
クライアントは、次回以降のリクエストに、そのCookieをくっつけてリクエストを送ります。
Cookieを受けとったRailsは、Cookieを解読し、セッションデータを復元します。(先ほどの例であれば、アイテムがカートに追加されている状態)
これが、Railsが行っている状態管理の流れです。
ログインしたユーザーの状態管理
先ほどお話した状態管理は、あくまで「同じユーザー」ということを認識して、以前と同じ状態を再現しているだけです。
「このユーザーは○○だ」ということは認識していません。
「さっきも来たお客さん」程度の認識です。
なので、クライアントからのリクエストにCookieがないと、状態を再現できなくなります。
それではアプリとしては困るので、ログイン機能の出番です。
ログインすると、Railsのセッションsession[:user_id]に、userのidが保存されます。
user_idは、usersテーブルに保存されているidです。
このidによって、「どのユーザーか」を認識することができます。
つまり、「このCookieを渡してきたブラウザは○○さんだ」ということが理解できるようになります。
そうすると、user_idをもとに、関連づけられた他のテーブルからもデータを取得することができて、ログインしているユーザーのさまざまなデータを表示できます。
ちなみに、テーブルがないデータは、セッションsession[:key]に一時的に保存されるだけで、永続化されません。Cookieの期限とともに削除されてしまいます。
セッションクッキー(session cookie)
ここまでの状態管理に使われているCookieのことは、セッションクッキーといいます。
セッションクッキーは、有効期限を持っていません。
別の言い方をすると、セッションクッキーのexpiresオプションがnilになっています。
有効期限を持たない場合、一般的に、ブラウザを閉じるとセッションクッキーは破棄されます。
(ブラウザによって動作が異なる場合もあるようです)
そのためログインしても、セッションクッキーで状態を管理していると、ブラウザを閉じてセッションクッキーがなくなるとログインが切れてしまいます。
セッションクッキーは、ブラウザの検証ツールから確認できます。
アプリケーション >> ストレージ >> Cookie の中に、デフォルトであれば、「_アプリ名_session」みたいな名前のCookieがあると思います。
ログイン状態の保持
これまでは通常のログインでしたが、ここからは「ログイン状態の保持」をオンにしてログインしたときのお話です。
その前に、Rails7.2でgem"devise"を使った場合、どうやってログイン状態を保持する機能を実装するかのお話をします。
deviseの「ログイン状態を保持」する機能(rememberableモジュール)
通常どおりdeviseで実装を進めていくと、userモデルやログイン関係のビューファイルがいろいろ作られます。
ビューファイルには、すでに「ログイン状態を保持する」というチェックボックス付きのフォームが生成されると思います。
またuserモデルを見ると、以下のようになっています。
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
end
devise :...に書かれているのは、devise側が用意しているモジュールです。
詳細は割愛しますが、モジュールを追加するだけでdeviseが追加の機能を提供してくれます。
「ログイン状態の保持」は、初期から追加されている上記モジュールのうち、rememberableモジュールが担っています。
つまり、deviseを入れるだけで、すでに「ログイン状態の保持」が実装されているということです。
「ログイン状態を保持する」場合の状態管理
deviseで「ログイン状態を保持する」状態でログインしたらどうなるかについてのお話です。
通常の状態管理であれば、セッションクッキーが作られていました。
Railsのセッションのデータを暗号化して、そのCookieをブラウザに保存。
ただし、ブラウザを閉じると破棄されて、状態が再現できなくなるというものでした。
セッションクッキーはブラウザを閉じると破棄されるため、ログイン状態を維持できません。
この問題を解決するために、deviseのrememberablモジュールは、「記憶用の別クッキー(rememberクッキー)」 を発行します。
rememberクッキーも、セッションクッキーと同様に、ブラウザの検証ツールから「remember_user_token」という名前で確認できると思います。
ログインすると、deviseがremember_tokenを生成し、そのトークンに署名をつけて、それをrememberクッキーとして保存します。
※署名つきなので、改ざんされているかがサーバー側で検証できます。
ブラウザは、リクエストにrememberクッキーをつけてRailsに送り、Railsはトークンが有効か、userのidは存在するかなど確認して、問題なければ自動でログインさせます。
トークンの有効期限
ちなみに、トークンが有効かどうかは、設定されている期限によって判断されますが、deviseでは、その期限はデフォルトで2週間に設定されています。
config/initializers/devise.rbに以下のコードがあります。
デフォルトはコメントアウトされていたので、修正する場合はコメントインして修正すれば2週間から変更できます。
config.remember_for = 2.weeks
セッションクッキーの場合は、expiresオプションがnilのため、期限の設定がなく、ブラウザを閉じるとCookieが破棄されていましたが、rememberクッキーは、remember_forの設定によってexpiresオプションに期限が設定されます。
有効期限が2週間の場合、最初のログイン時にrememberクッキーが作られ、expiresオプションの期限が2週間後に設定されます。
(検証ツールから、CookieのExpiresを見ると、2週間後に設定されていることがわかります)
また、usersテーブルのremember_created_atカラムには、rememberクッキーが発行された日時が保存されます。
そして、remember_created_atに保存された日時から2週間(remember_forの値)が経過すると、トークンは無効となり、自動ログインは行われず、改めて通常のログインによる認証が必要となります。
このようにして、deviseは「ログイン状態の保持」を提供しています。
ログイン状態保持の確認をrememberクッキーで行い、自動でログイン処理が行われると、通常のログインと同様に、セッションクッキーが作られるので、以降はセッションクッキーによる状態管理が行われます。
おわりに
「ログイン状態の保持」について考えたことなかったので、この機会に勉強できてよかったです。
長くなってしまいましたが、最後まで読んでいただきありがとうございました。