0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails Tutorial 9章 ログイン機構の自分なり解釈まとめ

Last updated at Posted at 2019-09-04

#ユーザーを記憶するための手段(RailsTutorial9章)

##Cookiesメソッドを用いた永続ログインの方法
Cookiesメソッドによる、永続ログインの方法について、概要をバサッとまとめてみました。

  • 永続ログインを可能にするために、ブラウザのcookieに記憶トークンと呼ばれる文字列を保存する。

  • この記憶トークンをセキュアな文字列にした記憶ダイジェストをデータベースに保存する。

  • この記憶トークンをデータベースのセキュアな文字列と比較し、照合する。

  • 署名付き暗号化user_idもcookieに保存し、データベースのuser.idと比較する。

###記憶トークンの生成及び、ハッシュ値のデータベースへの保存
記憶トークンはSecureRandom の urlsafe_base64を使い生成
記憶ダイジェストは

/models/user.rb

def User.digest(string)
    cost=ActiveModel::SecurePassword.min_cost? Bcrypt::Engine::MIN_COST :
                                               Bcrypt::Engine.cost
    Bcrypt::Password.create(string, cost: cost)
end

のような関数でUser.digestの引数に代入した値をBcryptでハッシュ化する。
ここでは、インスタンスメソッドで定義する必要はないので、クラスメソッドで定義している。
引数に代入する値は生成したランダムな64文字の文字列である記憶トークンを用いる。

/models/user.rb

def User.new_token
    SecureRandom.urlsafe_base64
end

この二つの過程を合体させて生成した記憶トークンをハッシュ化した値をデータベースのremember_digestに保存する
この時、記憶トークンの属性も別途設定しておく

/models/user.rb

attr_accessor :remember_token


def remember
    self.remember_token=User.new_token
    Update_attribute(:remember_digest,User.digest(remember_token))
end

注意 self.remember_tokenとしないとremember_tokenのローカル変数を定義してしまうことになる。

###current_userにログイン中のユーザー情報を保持させる

1.sessionがまず存在するかどうかを確かめる

2.ない場合はcookiesに署名されたuser_id (つまりcookies.signed[:user_id])が存在するか確かめる

3.user=User.find_by(id:cookies.signed[:user_id])でUser モデルにcookiesのuser_idをもつユーザーが存在するか確かめる
注意 cookies.signed[:user_id]は暗号化されているが、自動的に暗号化が解除されている。

4.このuserが存在、つまりはtrueであり、かつ、そのuserのremember_digestとcookies[:remember_token]のハッシュ値が一致するとき、つまり

if user&& user.authenticated?(cookies[:remember_token]) ・・・・・注意
@current_user=user
def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

としてcookiesの中のremember_tokenキーに含まれるトークンのハッシュ値が、userのremember_digestと一致するとき、
ログイン中にする。(@current_userにuserを代入する。)

1. ー4.をまとめてコードにすると

def current_user
    if (user_id=session[:user_id])
        @current_user||=User.find_by(id: user_id) #@current_userが存在する場合はそのまま、ない場合はデータベースからActive Recordのfind_byメソッドを用いて検索
    elsif (user_id=cookies.signed[:user_id])
        user=User.find_by(id: user_id)
        if (user && user.authenticated?(cookies[remember_token]))#userが存在し、cookiesに保存されているトークンのハッシュ値とremember_digestが一致する場合
            @current_user=user
        end
    end
end

注意:ここでuser_id=session[:user_id]とあるがこれは比較をしているのではなく、
user_idにsession[:user_id]を代入した際に存在すれば。の意味である。

また、ヘルパーでcookiesに署名付き暗号化user.idを保存し、64文字のトークンを保存することで、
cookiesからユーザー情報をトークンで取り出せるようにする。

app/helpers/session_helper.rb

def remember(user)
    user.remember #データベース側にremember_digestを保存
    cookies.permanent.signed[:user_id]=user.id #署名付き暗号化user_idをcookies側に保存
    cookies.permanent[:remember_token]=user.remember_token #remember_tokenをcookies側に保存
end

これでcurrent_userにログイン中のユーザーの記憶する手段が整った。

###ログイン時に行われるログインの永続化の処理

/session_controller.rb

def create
    user=User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticated?(params[:session][:password])
        log_in (user) #ヘルパーで定義 #current_userにuserを代入
        remember(user)#ヘルパーで定義 #cookiesにremember_token及びuser_idを保存
        redirect_to user_url(user)
    else 
        flash.now[:danger]="Invalid email/password combination"
        render 'new'
    end
end

##Logoutするとき
ログアウトするときは、

  • cookies.signed[:user_id]の削除
  • cookies[:remember_token]の削除
  • remember_digestをnilにする
  • sessionの情報も破棄する
  • current_userもnilにする
models/user.rb
def forget
 update_attribute(:remember_digest,nil)
end
helpers/sessions_helper.rb
def forget(user)
 user.forget
 cookies.delete(:user_id)
 cookies.delete(:remember_token)
end
helpers/sessions_helper.rb
def logout
  forget(current_user)
  session.delete(:user_id)
  @current_user=nil
end
helpers/sessions_helper.rb
  def destroy
    log_out if logged_in? 
    redirect_to root_url
  end

ここで

log_out if logged_in?

としている理由としては
ブラウザが2つ起動していて、片方のブラウザでログアウト処理をした場合に、current_userがnilになるのでforget(current_user)の引数にしているオブジェクトがnilになるのでエラーをはくためです。

もう一つエラーがあります。
同じようにブラウザが2つ起動していて、片方でログアウト処理をしたとき、remember_digestの値がnilになるので、もう片方のブラウザで一度ブラウザを閉じて、再びつけたときに、cookieは存在するので、下の式のcookies.signed[:user_id]が評価されるので次のif文まで評価されますが、authenticated?メソッドで使っているBcrypt.newは引数であるremember_digestがnilになってしまいエラーになってしまいますので、remember_digestがnilの場合はfalseを返すようにします。

def current_user
  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])# BCryptで生成するdigestの値がnilとなりエラーが発生する。
      log_in user
      @current_user = user
    end
  end
end
def authenticated?(remember_digest)
 return false if remember_digest.nil? #remember_digestがnilの場合はfalseを返して、条件式を終了させる。
 BCrypt.new(remember_digest).is_password?(remember_token)
end

##まとめ

  • cookiesではログインするための情報を保存する

  • 署名付き暗号化user_id

  • 記憶トークン

  • ログインするときは、sessionから照合し、ない場合は、cookiesの情報を参照する。

  • データベースにはハッシュ化されたトークンを保存しておき、authenticated?ヘルパーにより、cookiesの記憶トークンと照合する。

  • ログアウトするときはcookiesやデータベースのremember_digestをnilにする。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?