はじめに
こんにちは。しばらく投稿の間が空いてしまいました…。
言い訳をしておきますと、(また後で記事にできたらいいなと思っているのですが…)「要件定義」や「基本設計」について、深掘りして勉強しながら、実際にオリジナルアプリの設計をしていたからです。
暫定的な成果物はできたのですが、おそらく、アプリ開発の中で、見直しされていくと思いますので、最終的にアプリが形になってから、記事にしたいと思います。
今回は、どのWebアプリでも必ず実装するであろう「ログイン機構」について、学んだことをまとめていきたいと思います。
何を書いたか
Ruby on rails チュートリアルの第8章、第9章の内容をベースに、自分なりに咀嚼したものを描いていきます。
なぜ書いたか
学習記録です。特に自分が理解しにくかった部分は、自分なりの解釈を加えています。(間違っている部分はご指摘いただけると幸いです。)
本題
基本的なログイン機構
ログインの基本的な仕組み
→ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み
→これを「認証システム(Authorization System)」と呼ぶ
ログイン済みのユーザー(current user)だけがアクセスできるページや、扱える機能などを制御
→このような制限や制御の仕組みを「認可モデル(Authorization Model)」と呼ぶ
・ログインしたユーザーだけがユーザーの一覧ページに移動できるようにする
・正当なユーザーだけが自分のプロフィール情報を編集できるようにする
・管理者だけが他のユーザーをデータベースから削除できるようにする
これら全て「認証システム」と「認可モデル」で実現できる。
また、ログイン済みユーザー(current user)を利用して、
・ユーザーのIDとマイクロポストを関連付ける
・他のユーザーをフォローする機能や、自分のフィード一覧を実装
ということも実現できる。
HTTP
・ステートレス(Stateless)なプロトコル
・HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクション
・つまり、忘れっぽいプロトコル
セッション(session)
・コンピュータ間(ユーザーのパソコンのWebブラウザとRailsサーバーなど)の半永続的な接続
・HTTPプロトコルと階層が異なる(上の階層にある)ので、HTTPの特性とは別に接続を確保できる
クッキー(cookies)
・Railsでセッションを実装する方法として最も一般的
・ユーザーのブラウザに保存される小さなテキストデータ
・別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できる
・アプリはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができる
sessionsコントローラーの概要
・newアクション → ログインページを生成し、新しいセッションを出力
・createアクション → ログイン操作で、セッションを実際に作成して保存
・destroyアクション → ログアウト操作で、セッションを破棄
※Usersリソースと異なるのは、UsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使う
HTTPリクエスト | URL | 名前付きルート | アクション名 | 用途 |
---|---|---|---|---|
GET | /login | login_path | new | 新しいセッションのページ(ログイン) |
POST | /login | login_path | create | 新しいセッションの作成(ログイン) |
DELETE | /logout | logout_path | destroy | セッションの削除(ログアウト) |
ログインフォーム
・ユーザー登録ではデータベースに情報を渡したため、Active Recordによって自動生成されるメッセージを使ったが、セッションはActive Recordオブジェクトではないので、Active Recordがエラーメッセージを表示してくれるということはない。→フラッシュメッセージでエラーを表示
セッション実装の手順
・ログインで入力が無効な場合の処理
→ログインが失敗した場合に表示されるエラーメッセージを配置
→ログインに成功した場合に使う土台部分を作成(ここではログインが送信されるたびに、パスワードとメールアドレスの組み合わせが有効かどうかを判定)
paramsメソッドの使い方
・paramsメソッドはハッシュ構造をもち、ハッシュと同様に扱える。
※あくまでハッシュのように扱えるだけで、実はメソッドである。
・createアクションの中では、ユーザーの認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる。
例えば、以下のように使う。
params[:session][:email]
params[:session][:password]
createアクションの実装
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
# エラーメッセージを作成する
render 'new'
end
end
→ if user && user.authenticate(params[:session][:password])
→ has_secure_passwordはauthenticateメソッドを提供する。(authenticateメソッドは認証に失敗したときにfalseを返す)
→ userがtrueかつuser.authenticate(***)がtrueになる場合のみif文の処理が実行される
ヘルパーメソッド
・複数のコントローラからログイン関連のメソッドを呼び出せるようにする。
・ヘルパーはコントローラを生成したときに自動的に用意されるので、あとは「どこから呼び出せるようにしたいか」を決めるだけ。
・ログイン機構はあらゆる場面で使うことになるので、全コントローラの親クラスである「Applicationコントローラ」に自動生成されたセッション用のヘルパーを読み込ませ、どのコントローラからでもログイン関連のメソッドを呼び出せるようにする。
class ApplicationController < ActionController::Base
include SessionsHelper
end
sessionメソッド(※Sessionsコントローラーとは無関係)
session[:user_id] = user.id
・sessionメソッドはハッシュのように扱える
・上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される
・session[:user_id]を使ってユーザーIDを元通りに取り出せる
・sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了
current_userメソッド
・セッションIDに対応するユーザー名をデータベースから取り出せるようにしたい
User.find(session[:user_id])
#=>このコードではユーザーIDが存在しないと「例外」が発生し、エラーになる(ActiveRecord::RecordNotFound)
#=>ユーザーがログインしていない状況が考えられるケースでは、session[:user_id]の値はnilになり得る
User.find_by(id: session[:user_id])
#=>このコードを使うと、IDが無効な場合(=ユーザーが存在しない場合)にメソッドは例外を発生せず、nilを返す
def current_user
if session[:user_id]
User.find_by(id: session[:user_id])
end
end
これは次のように書き換えられる
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
#@current_userがnilまたはfalseであれば、User.find_by(id: session[:user_id])の値を返す
end
end
発展的なログイン機構
永続cookie(permanent cookies)
・任意でユーザーのログイン情報を記憶しておき、ブラウザを再起動した後でもすぐにログインできる機能→ "remember me"
記憶トークンと暗号化
・Railsのsessionメソッドを使ってユーザーIDを保存した場合、ブラウザを閉じると情報が消えてしまう。
・記憶トークン(remember token)を生成し、cookiesメソッドによる永続的cookiesを作成する。
・記憶ダイジェスト(remember digest)によるトークン認証に記憶トークンを活用
※「トークン」とは、パスワードの平文と同じような秘密情報。パスワードとトークンとの一般的な違いは、パスワードはユーザーが作成・管理する情報であるのに対し、トークンはコンピューターが作成・管理する情報。
・sessionメソッドで保存した情報は自動的に安全が保たれるが、cookiesメソッドに保存する情報はその限りではない。
→cookiesを永続化するとセッションハイジャックという攻撃を受ける可能性があるため対策が必要。
new_tokenメソッドをクラスメソッドとして定義
・ユーザーオブジェクトが不要なので、Userモデルのクラスメソッドとして作成
class User < ApplicationRecord
.
.
.
# ランダムなトークンを返す
def User.new_token
SecureRandom.urlsafe_base64
end
end
rememberメソッド
・user.remember
→ユーザーと関連付いた記憶トークンを生成し、トークンに対応する記憶ダイジェストをデータベースに保存
・update_attributeメソッドはバリデーションを素通りさせる。(ユーザーのパスワードやパスワードはPCが行うため、間違う可能性は理論的ににゼロである。)
class User < ApplicationRecord
attr_accessor :remember_token
.
.
.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
end
cookiesメソッド(記憶トークンの永続化)
・sessionのときと同様にハッシュとして扱える。
・個別のcookiesは、1つのvalue(値)と、オプションのexpires(有効期限)からなる。
cookies[:remember_token] = { value: remember_token,
expires: 20.years.from_now.utc }
#上のコードは以下のようにシンプルに書き換えられる。
cookies.permanent[:remember_token] = remember_token
#上のコードによって、Railsは期限を20.years.from_nowに設定
cookiesメソッド(暗号化済みユーザーIDの永続化)
・攻撃者がユーザーアカウントを奪い取れないように、署名付きcookieを使う。
cookies[:user_id] = user.id
#sessionメソッドと同じパターン。しかしこれでは、IDが生のテキストとしてcookiesに保存されてしまう。
cookies.signed[:user_id] = user.id
#上のコードによって、cookieをブラウザに保存する前に安全にユーザーIDを暗号化できる
cookies.permanent.signed[:user_id] = user.id
#signedとpermanentをメソッドチェーンで繋いで使い,暗号化したユーザーIDを永続化する
User.find_by(id: cookies.signed[:user_id])
#上のように、cookiesからユーザーを取り出せるようになる。cookies.signed[:user_id]では自動的にユーザーIDのcookiesの暗号が解除。
記憶トークンとユーザー記憶ダイジェストの一致確認
class User < ApplicationRecord
・
・
・
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
end
・authenticated?メソッド中にローカル変数として定義したremember_tokenは、attr_accessor :remember_tokenで定義したアクセサとは異なる点に注意。
・is_password?の引数はメソッド内のローカル変数を参照している。
・remember_digestの属性の使い方は、self.remember_digestのself.省略形。
ユーザーのcookiesを永続化
・rememberメソッドとremember(引数)メソッドは別物
・remember(引数)メソッドでは、
→引数で渡されたユーザーの新しい記憶トークンを生成
→記憶トークンをハッシュ化し、データベースに登録
→ユーザーIDを暗号化し、永続cookiesに保存
→記憶トークンを永続cookiesに保存
module SessionsHelper
・
・
・
# ユーザーのセッションを永続的にする
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
・
・
・
end
current_userメソッドの改良
・永続セッションの場合は、session[:user_id]が存在すれば一時セッションからユーザーを取り出し、それ以外の場合はcookies[:user_id]からユーザーを取り出して、対応する永続セッションにログインする必要がある。
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:user_id]
user = User.find_by(id: cookies.signed[:user_id])
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
#上のコードではsessionメソッドもcookiesメソッドもそれぞれ2回ずつ使われてしまい、無駄。
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
#上のコードのようにローカル変数user_idに代入することで、問い合わせ回数をそれぞれ1回にする。
forgetメソッド
・ユーザーを忘れるためのメソッドを定義する。
・方針は記憶ダイジェストをnilで更新する。
class User < ApplicationRecord
・
・
・
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
end
ヘルパーメソッドも定義しておく。
module SessionsHelper
・
・
# 永続的セッションを破棄する
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
# 現在のユーザーをログアウトする
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
end
参考文献
Ruby on rails チュートリアル(第8章、第9章)
最後に
sessionやcookiesを使ったログイン機構の基本を勉強しました。
気になったRubyの文法も別途まとめておきたいと思います。