しおブログ言うてますけど

技術ブログ言うてますけど技術以外のことも発信すると思われ。RubyとかRuby on Railsとか生き方とかジャンルにこだわらず自分の好きなように。初心者なのでお手柔らかに。

ログイン機能の実装(ログイン情報を取得するまで②)

この記事は自分が理解しにくかったログイン機能の仕組みについてアウトプットする記事です。
一つの記事にすると長くなりすぎるのでいくつかの記事に分けて記事を書いてるため、参考にする方はログイン機能の実装(digestを保存するまで①) を先に読んでください。

ログイン機能の実装(digestを保存するまで①)でやったこと

  • ログイン機能を実装する目的はWEBアプリケーションにて、自分以外のユーザーが他のユーザーの情報を操作できないようにするためである。
  • ユーザー(User)モデルを作成する際、属性には必ず必要なname, email, password_digestとする。
  • password_digestにする理由はRails標準のhas_secure_passwordという機能を使うためである。
  • モデルにhas_secure_passwordと記述すると、passwordpassword_confirmationの2つのデータベースのカラムには対応しない属性が追加される。
  • password属性の値とpassword_confirmation属性の値が一致しているとusersテーブルのpassword_digestカラムにハッシュ化されたパスワードが保存される。

ログイン機能の実装

これからログイン機能を作成する。ログイン機能はユーザーがログインするためにのフォーム画面を表示し、送られてきた情報を元にユーザーを認証する。ログイン機能と別にログアウト機能も提供する。
現場Railsではログイン機能についてこういう風に解説している。

ログイン機能を実装する際は、ログインする=「セションリソースを作る」と捉えて、SessinsControllerという名前でコントローラーを作ることがよく行われています。

なるほど。とりあえずSessionsControllerを作成してそこからログイン機能を作るということだと思う。
SessionsControllerに追加したいアクションは下記を参照する。

アクションの内容 HTTPメソッド URL アクション名
ログインのフォームを表示する GET /login new
フォームから送られてきた情報を元にログインを行う POST /login create
ログアウトを行う DELETE /logout destroy

早速SessionsControllerをrails gコマンドで作成する。

$ rails g controller Sessions new create destroy

コマンドによりルーティングが設定されましたが、ログインフォームでは/loginというURLにするので routes.rbを下記のように編集する。

Rails.application.routes.draw do
  get '/login',  to: 'sessions#new'
  post '/login', to: 'sessions#create'
  get '/logout', to: 'sessions#destroy'
  ...

end

これでログインフォームを表示するアクションのURLが/loginになり、ログアウトのリクエストも/logoutになる。

ログインフォームの作成

app/views/sessions/new.html.silmを編集して、ログインフォームの作成をする。

h1 ログイン

= form_with scope: :session, local: true do |f|
  .form-group
    = f.label :email, 'メールアドレス'
    = f.text_field :email, class: 'form-control', id: 'session_email'
  .form-group
    = f.label :password, 'パスワード'
    = f.password_field :password, class: 'form-control', id: 'session_password'
  = f.submit 'ログインする', class: 'btn btn-primary'

このように編集することでログインフォームが完成する。

実際にログインする

ログインフォームができたので実際にログイン機能を開発していく。
先程 rails g コマンドで作成したときに作成されたsessionsコントローラーのcreateアクションを編集する。

  def create
    user = User.find_by(email: session_params[:email])

    if user&.authenticate(session_params[:password])
      session[:user_id] = user.id
      redirect_to root_url, noitce: 'ログインしました'
    else
      render 'new'
    end
  end
  .....


  private
  def session_params
    params.require(:session).permit(:email, :password)
  end

まずuser = User.find_by(email: session_params[:email])で送られてきたメールアドレスでユーザーを探す。
ユーザーが見つかった場合はauthenticateメソッドを使い認証を行う。
ここでauthenticateメソッドが出てきたが、authenticateメソッドはhas_secure_passwordと記述したときに追加される認証のためのメソッドである。
引数で受け取ったパスワードをハッシュ化してUserオブジェクト内部に保存されているdigestと一致するかを調べ、一致していたら認証が成功し、Userオブジェクト自身を返す。
一致していなければfalseを返す。

※ぼっち演算子について

if user&.authenticate(session_params[:password])&.がなんなのか最初全くわからなかった。
これはRubyのぼっち演算子と呼ばれる書き方で正式にはsafe navigation operatorと言われるものらしい。
Rubyにおいて、レシーバーであるオブジェクトに対してメソッドを実行する時、オブジェクトがnilの場合はエラーになる。 実際にrails c を使って調べてみる。

/変数foobarにnilを代入する/

> foobar = nil
=> nil

/nilオブジェクトに対してメソッドを実行する/

> foobar.downcase
Traceback (most recent call last):
NoMethodError (undefined method `downcase' for nil:NilClass)

このようにnilが代入されたオブジェクトに対してメソッドを実行するとエラーになることがわかる。
こういった場合にぼっち演算子を使用し、レシーバーがnilのときはエラーを出すことなく、そのままnilを返すというものである。

/先程nilを代入したfoobarにぼっち演算子を使用した例/

foobar&.downcase
=> nil

上記のようにエラーならずnilが返ってくることがわかる。
オブジェクト&.メソッドでぼっち演算子を使用することができる。
つまりif user&.authenticate(session_params[:password])ではメールアドレスに対応するユーザーのデータが見つからないときはuserはnilを返すので、ユーザーデータが見つからない場合はif文のelseに飛ぶということである。
session[:user_id] = user.idでは認証に成功した場合、セッションにuser_idを格納している。
誰もログインしていない状態。session[:user_id]がnil
誰かがログインしている状態。session[:user_id]にログイン中のユーザーIDが入っている
ということが言える。


ログイン情報の取得

ユーザーがログインしていればsession[:user_id]にユーザーのIDが格納されるので下記のようにユーザー情報を取得することができる。

User.find_by(id: session[:user_id])

現場Railsではこの処理を頻繁に使うためにこのように解説している。

このようなログインしているユーザーを取得する処理は、頻繁に必要になるので、コントローラーやビューから簡単に呼べるようにするのが定石になっています。

すべてのコントローラーからメソッドを使えるようにするにはapplication_controller.rbにメソッドを定義することになるので、application_controller.rbを編集する。

class ApplicationController < ActionController::Base
  helper_method :current_user

  private
  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end
end

ここではすべてのコントローラーとすべてのビューのから使えるようにするため、helper_methodを指定している。

※||=(nilガード)について

@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]||=がなんなのか理解が浅いので詳しく調べてみる。

これは変数に値を入れるときに、変数がnilかfalseのときのみ値を入れる、というものらしい。
rails c で調べてみる。

/test1に文字列foobar を代入する/

> test1 = "foobar"
=> "foobar"

/nilでもfalseでもない値に||=を使って文字列hogehogeを代入する/

test1 ||= "hogehoge"
=> "foobar"

変数test1はfoobarが代入されており、その状態で||= を使ってもnilでもfalseでもないのでhogehogeは代入されない。
通称これは「nilガード」と言われるものである。
ではnilが代入されていたらどうなるのか。

/test2にnilを代入する/

> test2 = nil
=> nil

/nilが入った変数に||= を使ってhogehogeを代入する/
test2 ||= "hogehoge"
=> "hogehoge"

> test2
=> "hogehoge"

このように||= ではnilまたはfalseが入った変数に対して代入するというものである。

ここまでのまとめ

  • ログイン機能を実装する際は、ログインする=「セションリソースを作る」と捉えて、SessinsControllerという名前でコントローラーを作る。
  • URLは/login、/logoutになるようにルーティングを変更する。
  • authenticateメソッドはhas_secure_passwordと記述したときに追加される認証のためのメソッド
  • &.(ぼっち演算子)は、あるオブジェクトがnilの場合、そのままnilを返しnilでない場合はオブジェクトを返す。
  • User.find_by(id: session[:user_id])で簡単にログインしているユーザーを取得することができ、コントローラーやビューで呼べるようにするため、application_controller.rbにhelper_methodを指定してメソッドを定義する。
  • ||=(nilガード)は変数がnilまたはfalseのみ値を代入するというものである。
    少し長くなってしまったが、ログイン情報を取得するまでできるようになった。もう少し続くのでまたアウトプットとしてまとめて行きたいと思う。