じぶんメモ

プログラミングのメモ、日常のメモとか。

Railsでgemなしでログイン機能を実装

はじめに

Railsでは便利なログイン機能を実装してくれるdeviseやsorceryといったgemが存在します。 導入するだけでsign_insign_outといったログインに必要なメソッドを自動的に生成してくれますが、 カスタマイズをする場合には、内部のソースを解読する必要があったりします。 Rails4ではhas_secure_passwordという便利なメソッドが導入され、ログイン機能くらいならgemなしでも簡単に実装できるのでまとめてみました。

ログインの仕様を考える

よくあるログイン画面では、Emailとパスワードを入力させ、 その組み合わせが正しいかでチェックをします。 今回も同様にEmaiとパスワードを持つUserモデルを例にとります。

またユーザの登録時に、パスワードと確認用のパスワードを入力させ、 内容が一致すれば登録させる、というバリデーションをとります。

ユーザ登録機能の実装

Modelの実装

まずテーブルの作成ですが、mailとpassword_digestを用意します。 password_digestには、暗号化されたパスワードが登録されます。 password_digestはhas_secure_passwordに使用される名前なので、他の名前にはしないで下さい。 remember_tokenはログイン時に発行するトークンを保持し、cookieにも同様の値を保持させ、 ログインしているかどうかを判別するのに使用します。

create_table "users", force: :cascade do |t|
  t.string   "name",              limit: 191,             null: false
  t.string   "mail",              limit: 191,             null: false
  t.string   "password_digest",   limit: 191,             null: false
  t.string   "remember_token",    limit: 191
  t.datetime "created_at",                                null: false
  t.datetime "updated_at",                                null: false
end

続いてモデルの作成です。

class User < ActiveRecord::Base
  has_secure_password validations: true

  validates :mail, presence: true, uniqueness: true
end

has_secure_passwordを宣言することで、password, password_confirmationをUserモデルのプロパティとして使用することができます(DB上での管理ではなくメモリ上で値を保持できるようになる)。 引数にオプションとしてvalidations: trueが与えられているが、trueを渡すことによって、 以下のバリデーションがUserモデルに追加されます。

  • userの新規登録時にpasswordの必須入力
  • passwordとpassword_confirmationの内容が合致すること

また、それとは別にメールアドレスを必須入力かつユニークになるようバリデーションを設定しています。

Viewの実装

ログインユーザの作成画面を作ります。

= form_for @user, url: users_path do |f|
  = form_error(@user)
  .form-group
    = form.label :name
    = form.text_field :name

  .form-group
    = form.label :mail
    = form.text_field :mail

  .form-group
    = form.label :password
    = form.text_field :password

  .form-group
    = form.label :password_confirmation
    = form.text_field :password_confirmation

  = f.submit '登録'

名前・メール・パスワード・パスワード確認の入力欄を設けます。

Controllerの実装

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to login_path
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :mail, :password, :password_confirmation)
    end

end

入力されたパラメータを使用して登録をします。 この時、以下の検証が行われ、エラーがなければユーザが登録されます。

  • アドレスが未入力の場合はエラー
  • 入力されたアドレスが他のユーザに使用されている場合はエラー
  • パスワードが未入力の場合はエラー
  • パスワードとパスワード確認の内容が一致していない場合はエラー

入力されたパスワードは、password_digestに暗号化されて登録されます。

ログイン機能の実装

ユーザ登録が済んだら、ログイン機能の作成を行います。

ルーティング設定

Rails.application.routes.draw do
  # ログイン / ログアウト
  get     'login',   to: 'sessions#new'
  post    'login',   to: 'sessions#create'
  delete  'logout',  to: 'sessions#destroy'
end

メール・パスワードを入力させるsessions#new、 入力された情報を検証し、cookieにログイン情報を格納するsessions#create、 ログアウトさせるsessions#destroyをとります。

viewの作成

= form_for :session, url: login_path do |f|
  .form-group
    = f.text_field :mail
  .form-group
    = f.password_field :password

  = f.submit 'ログイン'

アドレスとパスワードを入力させます。

controllerの実装

class SessionsController < ApplicationController
  before_action :require_sign_in!, [:destroy]
  before_action :set_user, only: [:create]

  def new
  end

  def create
    if @user.authenticate(session_params[:password])
      sign_in(@user)
      redirect_to root_path
    else
      flash.now[:danger] = t('.flash.invalid_password')
      render 'new'
    end
  end

  def destroy
    sign_out
    redirect_to login_path
  end

  private

    def set_user
      @user = User.find_by!(mail: session_params[:mail])
    rescue
      flash.now[:danger] = t('.flash.invalid_mail')
      render action: 'new'
    end

    # 許可するパラメータ
    def session_params
      params.require(:session).permit(:mail, :password)
    end

end

一つずつ解説します。

1.newでログイン画面を表示

これは特に何もしてないので飛ばします。

2.ログイン画面で入力された値をcreateアクションで検証

  before_action :set_user, only: [:create]

  def create
    if @user.authenticate(session_params[:password])
      sign_in(@user)
      redirect_to root_path
    else
      flash.now[:danger] = t('.flash.invalid_password')
      render 'new'
    end
  end

before_actionでメールアドレスからユーザの情報を取得し、 authenticateメソッドでパスワードの検証を行っています。 authenticateメソッドは、モデルでhas_secure_passwordを宣言していると自動的に使用できるようになり、 入力されたパスワードを暗号化し、DBに登録されているpassword_digestと一致するか検証します。 検証が通れば、application_controllerで実装しているsign_inメソッドを呼び出し、 remember_tokenを作成し、userモデルとcookieにセットし、ログイン後の画面に遷移します。 remember_tokenは、後々ログインしているかどうかの検証に使用します。 Userモデルにnew_remember_tokenを実装しておきます。 これでログインは完了です。

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:user_remember_token] = remember_token
    user.update!(remember_token: User.encrypt(remember_token))
    @current_user = user
  end
    def self.new_remember_token
      SecureRandom.urlsafe_base64
    end

    def encrypt(token)
      Digest::SHA256.hexdigest(token.to_s)
    end

3.ログアウトの処理

  def destroy
    sign_out
    redirect_to login_path
  end

sign_outcookieの中身のremember_tokenを削除します。

  def sign_out
    cookies.delete(:user_remember_token)
  end

ログインしていなかったらログイン画面に遷移させる。

ログインを実装していても、これをしていないと意味がありません。 ログアウトアクションは、ログインをしていないと使用できないと思いますのでこれを実装します。

  skip_before_action :require_sign_in!, only: [:new, :create]

ログインしていなかったらログイン画面に遷移させるrequire_sign_in!を実装します。 ログイン前のnew, createアクションでは実行させないようにしときましょう。 require_sign_in!の中身はapplication_controllerに実装します。 長くなるので、最終的なapplication_controllerの中身を以下に記します。

class ApplicationController < ActionController::Base

  before_action :current_user
  before_action :require_sign_in!
  helper_method :signed_in?

  protect_from_forgery with: :exception

  def current_user
    remember_token = User.encrypt(cookies[:user_remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

  def sign_in(user)
    remember_token = User.new_remember_token
    cookies.permanent[:user_remember_token] = remember_token
    user.update!(remember_token: User.encrypt(remember_token))
    @current_user = user
  end

  def sign_out
    @current_user = nil
    cookies.delete(:user_remember_token)
  end

  def signed_in?
    @current_user.present?
  end

  private

    def require_sign_in!
      redirect_to login_path unless signed_in?
    end

end

ログインしているかを判定するsigned_in?メソッドを別に切り出しておきます。 条件としては@current_userがセットされているか。 これをするために、before_action@current_userをセットするcurrent_userメソッドを定義します。

  def current_user
    remember_token = User.encrypt(cookies[:user_remember_token])
    @current_user ||= User.find_by(remember_token: remember_token)
  end

cookieからトークンを取得後暗号化し、cookieと同じトークンを持ったuserを取得します。 取得できなかった場合はログインしていない、と判断します。

最後に

メソッドの切り出しはケースバイケースなので、参考程度に、と考えて下さい。 重要なのは以下の点だと思います。

  • ログイン時はuserモデルにhas_secure_passwordを宣言することで使用できるauthenticate使って検証をかけること。
  • ログイン後はremember_tokenをcookieに、暗号化したremember_tokenをDBにセットしておいて、DBとcookieのremember_tokenが一致しているかでログインしているかを判定する。