Rails 総復習1ヶ月チャレンジ 3日目(Railsチュートリアル編)
3日目です!今日は8章 基本的なログイン機構のLog Inメソッドからです!
3日目(Ruby on Rails チュートリアル 8章 基本的なログイン機構 Log_Inメソッド〜)
ログイン機能続き
昨日からの続きからやっていきます。まずは実装の流れのおさらいです。
1: ブラウザを閉じるとログインを破棄する(Railsのsessionメソッドを用いる)
→ログインフォーム、Sessionsコントローラーまで実装済み
2: ユーザーのログイン情報を自動で保存する(Railsのcookiesメソッドを用いる)
3: ユーザーがチェックボックスをオンにした場合のみログインを保存する(Remember me)
では1の続き、Log_in メソッドからやっていきます。
- Log_inメソッド実装
ログイン手法を様々な場所で使い回せるようにSessionsヘルパーにlog_inメソッドを定義します。
#app/helpers/sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end end
注目はこの部分
session[:user_id] = user.id
Railsで事前定義済みのsessionメソッドを使いユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成されます。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができます。なおsessionsメソッドで作成された一時cookiesはブラウザで閉じた瞬間に有効期限が終了します。
Sessionsコントローラーのcreateアクションにlog_inメソッドを追加
#app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy end end
補足: ぼっち演算子
sessions_controllerのcreateアクションの&&部分は、Rubyの”ぼっち演算子"と呼ばれる&.の表記でよりシンプルに出来ます。ぼっち演算子を使ってオブジェクトを呼び出すとオブジェクトがnilでない場合はその結果を、nilの場合はnilをそのまま返します。
#app/controllers/sessions_controller.rb if user && user.authenticate(params[:session][:password])
if user&.authenticate(params[:session][:password])
- current_userメソッドを定義
ユーザーIDを一時セッションの中に安全に置けるようになり、今度はそのユーザーIDを別のページで取り出すことにする為、current_userメソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにします。
#app/helpers/sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す(いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end end
注目はこの部分
@current_user ||= User.find_by(id: session[:user_id]) end
まずUser.find(session[:user_id]) ではなくUser.find_by(id: session[:user_id])とfind_byを用いているのはsession[:user_id]はnilの可能性がある為,nilだった場合に例外ではなくnilをそのまま返すようにしています。
ちなみに上記の一行は元々はこの形
if @current_user.nil? @current_user = User.find_by(id: session[:user_id]) else @current_user en
これが三項演算子を用いて下記の形となります。
@current_user = @current_user || User.find_by(id: session[:user_id])
さらにRubyで「||=」(or equals)という代入演算子は下記ように短縮出来ます。
@current_user ||= User.find_by(id: session[:user_id]) end
- logged_inメソッドの定義
ユーザーがログインしているときとそうでないときでレイアウトを変更(具体的にはログインしている時とログインしていない時のヘッダーを変えるなど)したいのでまずはlogged_inメソッドを定義します。
#app/helpers/sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す(いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end end
ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_userがnilではないという状態を指します。これをチェックするには否定演算子、! を使っています。
ログイン中のユーザー用のレイアウトのリンクを変更します。
#app/views/layouts/_header.html.erb <header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", '#' %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>
- ユーザー登録の際にログインする ユーザー登録が完了すると自動でログインが出来るよう、Usersコントローラーにlog_inメソッドを追加します。
class UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end end
- ログアウト機能
ログアウト処理ではlog_inメソッドの実行結果を取り消します。つまりセッションからユーザーIDを削除します。
# app/helpers/sessions_helper.rb module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end . . . # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end end
注目は下記です。deleteメソッドを実行し、セッションからユーザーIDを削除しています。
session.delete(:user_id)
Sessonsコントローラーにlog_outメソッドを追加します。
#app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out redirect_to root_url end end
これでsessionメソッドを使ったログイン機能が完成しました。
ユーザーのログイン情報を自動でユーザーの任意で保存する(permanent cookies)
sessionを用いたログインではユーザーが一度ブラウザを閉じてしまうとcookiesは有効期限が終了してしまいます。そこで、永続cookie(permanent cookies)を使ってブラウザを再起動した後でもすぐにログインできる機能(remember me)を実装します。(ブログの冒頭で示したログイン機能の2.3部分)
なお2.3はさらに長い為、手順をさらに細分化します。
①記憶トークン用のカラムを用意
②ランダムな文字列を生成し、ハッシュ化する
③ハッシュ化した値を、DBのカラムに保存する
④ブラウザのcookiesに、暗号化したIDとTokenを保存する
⑤cookiesのIDを使って、ユーザーをDBから検索する。cookiesのTokenを認証し、同一ならセッションを復元
かなり長いですが頑張ります・・・
①記憶トークン用のカラムを用意
remember_tokenをハッシュ化したものを入れる為のremember_digestカラムをUserモデルに用意します。
$ bundle exec rails generate migration add_remember_digest_to_users remember_digest:string
$ bundle exec rails db:migrate
②ランダムな文字列を生成し、ハッシュ化する,③ハッシュ化した値を、DBのカラムに保存する
続いてランダムな文字列を生成します。今回はRuby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドを使います。またランダムな文字列をハッシュ化する為のUser.digestメソッドを合わせて実装します。
#app/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 # ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 end
仮想属性のremember_tokenをUserモデルに追加します。また記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存するrememberメソッド作成する。
#app/models/user.rb class User < ApplicationRecord attr_accessor :remember_token # 渡された文字列のハッシュ値を返す 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 end
注目は下記
def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end
ここではUser.new_tokenで生成されたランダム文字列を仮想属性remember_tokenに代入(selfをつけているのはRubyによってローカル変数を作成されるのを防ぐため、selfを与えると期待通りの属性が設定されます)。そしてremember_digestカラムにハッシュ化されたremember_tokenを入れています。なおupdate_attributeは条件に一致するモデルオブジェクトを更新するメソッドでバリデーションを無視するようになっています。なぜバリデーションを無視するものを使っているかというと、今回設定したtokenは全てコンピュータ側で設定し、人間が介在するところがなく失敗する可能性が極めて低い為、無理にバリデーションを走らせる必要がない為です。
④ブラウザのcookiesに、暗号化したIDとTokenを保存する
ハッシュ化したTokenをDBに保存までできたので次は実際にログイン状態の保持をしていきます。その為引数付きのrememberメソッドを新たにSessonsヘルパーを作成します。
#app/helpers/sessions_helper.rb module SessionsHelper : # ユーザーのセッションを永続的にする def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end :
permanentはcookiesの有効期限を永続的(最長20年)付与します。signedは[:user_id]を暗号化しています。
⑤cookiesのIDを使って、ユーザーをDBから検索する。cookiesのTokenを認証し、同一ならセッションを復元
まずauthenticate?メソッド作成します。
#app/models/user.rb class User < ApplicationRecord : # 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end end
次にセッションの復元をします。
#app/helpers/sessions_helper.rb : # 記憶トークンcookieに対応するユーザーを返す 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 &.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end :
これでセッションの復元までは完了しました。
- forgetメソッドを実装 しかしこのままだと一度ログインしたら永続セッションが続いてしまうので終了できるメソッドを実装します。
#app/models/user.rb : # ユーザーのログイン情報を破棄する def forget update_attribute(:remember_digest, nil) end :
また引数付きのforgetメソッドをSessions_helperに定義します。そしてそれをlog_outメソッドから呼び出します。
#app/helpers/sessions_helper.rb . . # 永続的セッションを破棄する 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 :
Sessionsコントローラーのログアウトを少し変更します。
class SessionsController < ApplicationController . . . def destroy #ログイン中の場合のみログアウトする log_out if logged_in? redirect_to root_url end end
- remember meチェックボックス
ユーザーが任意に永続化cookiesにするか選択できるようにします。
ログインフォームに追加
#app/views/sessions/new.html.erb <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> : <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> : </div> </div>
ログインフォームから送信されたparamsハッシュには既にチェックボックスの値が含まれています。チェックボックスがオンの時に'1'になり、オフのときに'0'になります。 その値を使いコントローラーを変更します。
#app/controllers/sessions_controller.rb class SessionsController < ApplicationController : def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_to user : end end : end
これで実装完了です!次回は10章からやっていきます!