Railsでgemなしでログイン機能を実装
はじめに
Railsでは便利なログイン機能を実装してくれるdeviseやsorceryといったgemが存在します。
導入するだけでsign_in
やsign_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_out
でcookieの中身の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を取得します。 取得できなかった場合はログインしていない、と判断します。
最後に
メソッドの切り出しはケースバイケースなので、参考程度に、と考えて下さい。 重要なのは以下の点だと思います。