Railsのログイン機能(Device)で、remember_meのチェックをしたときにだけログイン状態が継続するような仕組みを作るために苦戦したので備忘。
やりたいこと
remember_meのチェックをしたとき、ログインのURLに遷移しないようにする。(ログインユーザの画面に遷移するようにする)
チェックをしなければ、ログインのURLに遷移するようにする。
やったこと
Userモデルにremember_meを有効にするコードを追加、更に、remember関数とそれに付随する関数を追加
class User < ActiveRecord::Base
attr_accessor :remember_token
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :reserves, class_name: 'Reserve'
validates :user_name, presence: true
validates :email, presence: true
# remember_meにチェックを付けた場合は、自動的にログインされる・・・はず
def remember_me
true
end
# 渡された文字列のハッシュ値を返す
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# ランダムなトークンを返す
def User.new_token
SecureRandom.urlsafe_base64
end
# 永続セッションのためにユーザーをデータベースに記憶する
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
# ユーザーのログイン情報を破棄する
def forget
update_attribute(:remember_digest, nil)
end
end
結果、下記のエラーが発生しました。
Railsチュートリアルを見ると、「remember_digest属性をUserモデルに追加」する必要があることに気づきました。チュートリアルに沿ってremember_digest属性をUserモデルに追加し、
DeviseのSessionControllerを下記のようにコーディングしました。
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
include ApplicationHelper
# GET /resource/sign_in
#def new
#end
# POST /resource/sign_in
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.valid_password?(params[:session][:password])
log_in(user)
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
# ログインユーザの遷移先
if user.admin == true
redirect_to "/admin/controller02"
else
redirect_to "/controller02"
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
# DELETE /resource/sign_out
def destroy
log_out if logged_in?
redirect_to "/users/sign_in"
end
end
しかし、これではremember_meのチェックをしたとしても、再度ログイン画面に遷移してしまいます・・・
ログを確認すると、下記のような挙動となっていました。
Started GET "/users/sign_in" for 113.149.235.78 at 2022-09-28 23:44:14 +0000
Processing by Users::SessionsController#new as HTML
Rendering layout layouts/application.html.erb
Rendering devise/sessions/new.html.erb within layouts/application
Rendered devise/shared/_links.html.erb (Duration: 0.3ms | Allocations: 166)
Rendered devise/sessions/new.html.erb within layouts/application (Duration: 3.1ms | Allocations: 1066)
Rendered layout layouts/application.html.erb (Duration: 26.4ms | Allocations: 15373)
Completed 200 OK in 31ms (Views: 29.6ms | Allocations: 16671)
つまり、Users::SessionsControllerのnewメソッドが処理されていることになります。
そこで、Users::SessionsControllerのnewメソッドを追加しました。
class Users::SessionsController < Devise::SessionsController
include ApplicationHelper
def new
#current_userの存在有無で判定
if current_user
# ログインユーザの遷移先
if current_user.admin == true
redirect_to "/admin/controller02"
else
redirect_to "/controller02"
end
end
end
(続く)
これによって、current_userの存在有無とadminか否かで遷移先を変更することができました。
current_userはこちらで定義しております。
module ApplicationHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す(いる場合)
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
#受け取ったユーザーがログイン中のユーザーと一致すれば true を返す
def current_user?(user)
user == current_user
end
# ユーザーがログインしていれば true、その他なら false を返す
def logged_in?
!current_user.nil?
end
# 現在のユーザーをログアウトする
def log_out
forget(current_user)
session.delete(:user_id)
@current_user = nil
end
(続く)
しかし、ここで作業しながら別の問題に気づきました。。。
logout時にlog_out関数が呼び出されず、セッションが切れないという問題です。
そこで、改めてUsers::SessionsControllerに下記を追記しました。
class Users::SessionsController < Devise::SessionsController
include ApplicationHelper
#↓ここを追記
skip_before_action :verify_signed_out_user, raise: false
(続く)
さて、本題に戻りまして、
これでもremember_meをチェックしなくても、ログインユーザ指定の画面に遷移してしまいます。。。
何が問題か2日かけて考え、これで何とか実装できるようになりました!
class Users::SessionsController < Devise::SessionsController
#before_action :configure_sign_in_params, only: [:create]
include ApplicationHelper
skip_before_action :verify_signed_out_user, raise: false
def new
#↓current_userの情報をcookieで判定(ここが足りなかった!)
if current_user && current_user.authenticated?(cookies[:remember_token])
# ログインユーザの遷移先
if current_user.admin == true
redirect_to "/admin/controller02"
else
(続く)
cookieとセッションの違いについてはまだはっきりと分かっていないのですが、
参考にしたページ(ベースはRails tutorial第9章)を読み解くと、
Remember meのチェックしたか否かでcookieを保持するか、削除するかという挙動の違いがあることが分かりました。ですので、そのcookieを保持しているかの条件式を指定したい場所のコードに書けば良かった、ということでした!
結論
remember_digestを用いてRemember_meのチェックをしたときにだけ、指定のURLに遷移しないようにする方法としては、cookieの削除がされたか否かで遷移先を判定する方法がある。(セッションが削除されたか否かではない)
参考URL
https://railstutorial.jp/chapters/advanced_login?version=5.1
https://teratail.com/questions/245600
https://qiita.com/th_9plus/questions/e8365ce6b705399ea0d5
https://qiita.com/d0ne1s/items/f73e1725cc214af82587
https://qiita.com/dany1468/items/692fc51d2be58ddb857a
https://rennnosukesann.hatenablog.com/entry/2018/02/17/225921