33
25

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 3 years have passed since last update.

自動ログイン(Remember me)機能を実装する

Posted at

TODOアプリに、自動ログイン機能を追加します。

以前の記事で、sessionメソッドを使ってユーザーIDを保存しましたが、この情報はブラウザを閉じると消えてしまいます。

毎回ログインし直すのは少し不便なので、ログインフォームの「Remember me」チェックボックスにチェックをしてログインした場合には、再訪時に自動でログインできるように修正していきましょう。

##処理の流れ

  1. 「Remember me」にチェックを入れてログインした時、記憶トークン(ランダムな文字列)と記憶ダイジェスト(記憶トークンをハッシュ化したもの)を作成

  2. 記憶トークンと暗号化したユーザーIDをcookieに保存し(有効期限20年)、記憶ダイジェストをDBに保存する

  3. Sessonの無いユーザーがサイトを訪問した時、cookieにユーザーIDがあれば、そのIDをキーにDBを検索

  4. 検索して取得したユーザーの記憶ダイジェストと、cookieの記憶トークン(をハッシュ化したもの)を比較

  5. 一致すればSessionを開始する

  6. (明示的な)ログアウト時にcookieのユーザーID、記憶トークン、DBの記憶ダイジェストを全て削除する

※トークン:コンピューターによって作成、管理されるパスワードのこと

それでは、以下よりシステムを修正していきます。

##Modelの修正
###Userモデルにremember_digest属性を追加

$ rails g migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate

マイグレーション名の末尾を_to_usersにすることで、マイグレーションの対象がデータベースのusersテーブルであることをRailsに指示しています。

###remember_token属性を有効化
remember_token属性はDBには存在しない属性です。attr_accessorを使って、仮想的に利用できるようにします。

app/models/user.rb
class User < ActiveRecord::Base
  attr_accessor :remember_token
.
.

##Viewにチェックボックスを追加
フォームの後半に以下の記述を追加し、「Remember me」チェックボックスを表示します。
(後ほど、チェックが入っていた時にだけセッションを永続化する処理を実装していきます)

app/views/sessions/new.html.erb
<%= f.label :remember_me do %>
  <%= f.check_box :remember_me %>
  <span>Remember me on this computer</span>
<% end %>

行全体をラベルで囲んでいるので、行のどこの部分でもクリックできるようになっています。

##ログイン用メソッドを作成
Controllerでログイン永続化の処理を簡単に記述できるように、ログインに使う各種メソッドを作成していきます。
###トークン生成用メソッド
長くてランダムな文字列が作れれば方法は何でも構いません。ここではRuby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドを利用します。

app/models/user.rb
# ランダムなトークンを返す
def self.new_token #User.new_tokenと同じ意味
  SecureRandom.urlsafe_base64
end

###ハッシュ化用メソッド
記憶トークンをハッシュ化して記憶ダイジェストを作成するために、ハッシュ化用のメソッドを用意します。

app/models/user.rb
# 与えられた文字列のハッシュ値を返す
def self.digest(string) #User.digest(string)を同じ意味
  cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                BCrypt::Engine.cost
  BCrypt::Password.create(string, cost: cost)
end

###記憶トークンと記憶ダイジェストを生成するメソッド
上記で作成した2つのメソッドを使って、
・記憶トークンの生成→ユーザーに紐付け
・記憶ダイジェストの生成→DBに登録
の2つを行うメソッドを作成します。

app/models/user.rb
# 永続的セッションで使用するユーザーをデータベースに記憶する
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end

###記憶トークンと記憶ダイジェストを比較するメソッド
パスワードの認証の時には、authenticateメソッド(has_secure_passwordと記入するだけで作られるメソッド)を利用して、受け取ったパスワードとDBのパスワードダイジェストを比較しました。

今回は、比較用のメソッドを自分で作る必要があります。

app/models/user.rb
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

is_password?を使って比較しているのは、BCryptのソースコードを読み解いた結果このメソッドが使えることがわかったからです。

###ユーザーID、記憶トークン、記憶ダイジェストを登録するヘルパーメソッド
暗号化したユーザーIDと、記憶トークンをcookieに、記憶ダイジェストをDBに登録するヘルパーメソッドを作成します。
今回の実装の要となるメソッドです
ここまでのステップは、全てこのメソッドを作成するためにあったと言っても過言ではありません。

app/helpers/sessions_helper.rb
# ユーザーのセッションを永続的にする
def remember(user)
  user.remember #Userモデルで定義したrememberメソッド。記憶トークンを作成、ハッシュ化してDBに保存
  cookies.permanent.signed[:user_id] = user.id #ユーザーIDを暗号化してcookieに保存
  cookies.permanent[:remember_token] = user.remember_token #記憶トークンをcookieに保存
end

permanentを使ってcookieを永続化し(有効期限を20年に設定)、
signedを使ってユーザーIDを暗号化しています。

取り出しの際にはid = cookies.signed[:user_id]などで取り出すことができます。

##ログイン処理を修正
###ログイン後にrememberメソッドを実行
作成したrememberメソッド(ヘルパーメソッドの方)をログイン後に実行するよう、Createアクションを修正します。
チェックボックスにチェックがなかった場合に実行するforgetメソッドは、後ほど実装します。

app/controllers/sessions_controller.rb
def create
  user = User.find_by(email: params[:session][:email].downcase)
  if user && user.authenticate(params[:session][:password])
    log_in user
    #remember_meにチェックが入って入れば記憶トークン等を登録、チェックが無ければ記憶トークン等を削除
    params[:session][:remember_me] == '1' ? remember(user) : forget(user)
    redirect_to user
  else
.
.

###ログイン中のユーザーを取得するメソッドを修正
ログイン中のユーザーを取得するcurrent_userメソッドを、上から下のように修正します。

セッションがなくてもすぐにあきらめず、cookieのユーザーIDと記憶ダイジェストを使ってユーザーを取得するようになりました。

修正前

app/helpers/sessions_helper.rb
def current_user
  if session[:user_id]
   @current_user ||= User.find_by(id: session[:user_id])
  end
end

修正後

app/helpers/sessions_helper.rb
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])
      log_in user
      @current_user = user
    end
  end
end

ユーザーIDと記憶ダイジェストを使ってユーザーを取得した後には、log_inメソッドでセッションも開始しています。

##ログアウト用メソッドを作成
###記憶ダイジェストをnilで更新するメソッド

app/models/user.rb
# ユーザーのログイン情報を破棄する
def forget
  update_attribute(:remember_digest, nil)
end

###ユーザーID、記憶トークン、記憶ダイジェストを全て削除するメソッドを作成
作成したforgetメソッドを使って、セッション永続化に関わる全ての値を削除するヘルパーメソッドを作ります。

app/helpers/sessions_helper.rb
# 永続的セッションを破棄する
def forget(user)
  user.forget #Userクラスのforgetメソッド。DBの記憶ダイジェストにnilを登録。
  cookies.delete(:user_id) #cookieのユーザーIDを削除
  cookies.delete(:remember_token) #cookieの記憶トークンを削除
end

##ログアウト処理を修正

###ログアウト用メソッドを修正
作成したヘルパーメソッドforgetを、ログアウト用ヘルパーメソッドlog_outから呼び出します。

app/helpers/sessions_helper.rb
# 現在のユーザーをログアウトする
def log_out
  forget(current_user) #この行を追加
  session.delete(:user_id)
  @current_user = nil
end

##細かなバグを修正

現状、2つの細かいバグが存在するので、修正していきます。

###バグ1
1つのタブでログアウトし、もう1つのタブで再度ログアウトしようとするとエラーが発生する
→1度ログアウトすると(log_outメソッドを実行すると)、current_userの値がnilになるので、2回目のlog_outメソッドが失敗する。
→ログアウト前に、ログイン状態を確認するように修正する。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  def destroy
    log_out if logged_in? #この行を修正
    redirect_to root_url
  end
end

###バグ2
Firefoxでログアウトし、Chromeではログアウトせずにブラウザを終了させ、再度Chromeで同じページを開くとエラーが発生する
→片方のブラウザでログアウトしても、もう片方のブラウザにはcookie(ユーザーIDと記憶トークン)が残り続ける
→DBの記憶ダイジェストは削除されているのに、記憶トークンだけが残っている
→記憶トークンと記憶ダイジェストの比較の際にエラーが発生
→比較の前に、記憶ダイジェストの有無をチェックする

app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil? #この行を追加
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

##参考
第8章 ログイン、ログアウト - Railsチュートリアル

33
25
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
33
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?