LoginSignup
189
208

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-04-09

はじめに

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にも同様の値を保持させ、
ログインしているかどうかを判別するのに使用します。

Schemafile
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

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

user.rb
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の実装

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

users/new.html.slim
= form_for @user, url: users_path do |f|
  = form_error(@user)

  = f.label :name
  = f.text_field :name

  = f.label :mail
  = f.text_field :mail

  = f.label :password
  = f.text_field :password

  = f.label :password_confirmation
  = f.text_field :password_confirmation

  = f.submit '登録'

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

Controllerの実装

users_controller.rb
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に暗号化されて登録されます。

ログイン機能の実装

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

ルーティング設定

routes.rb
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の作成

sessions/new.html.slim
= form_for :session, url: login_path do |f|
  = f.text_field :mail
  = f.password_field :password

  = f.submit 'ログイン'

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

controllerの実装

sessions_controller.rb
class SessionsController < ApplicationController
  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を実装しておきます。
これでログインは完了です。

application_controller.rb
  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
user.rb
    def self.new_remember_token
      SecureRandom.urlsafe_base64
    end

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

3.ログアウトの処理

sesions_controller.rb
  def destroy
    sign_out
    redirect_to login_path
  end

sign_outでcookieの中身のremember_tokenを削除します。

application_controller.rb
  def sign_out
    cookies.delete(:user_remember_token)
  end

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

ログインを実装していても、これをしていないと意味がありません。
sessions_controllerのログアウトアクションは、ログインをしていないと使用できないと思いますのでこれを実装します。
ログインしていなかったらログイン画面に遷移させるrequire_sign_in!を実装します。
ログイン前のnew, createアクションでは実行させないようにしときましょう。

sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :require_sign_in!, only: [:new, :create]
  before_action :set_user, only: [:create]

また、ユーザ登録も、新規でサービスを利用する人が最初に行うオペレーションなので、こちらも同様にログインなしで実行できるようにします。

users_controller.rb
class UsersController < ApplicationController
  skip_before_action :require_sign_in!, only: [:new, :create]

  def new
    @user = User.new
  end

require_sign_in!の中身はapplication_controllerに実装します。
長くなるので、最終的なapplication_controllerの中身を以下に記します。

application_controller.rb
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が一致しているかでログインしているかを判定する。
189
208
5

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
189
208