はじめに
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)
= 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の実装
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|
= f.text_field :mail
= f.password_field :password
= f.submit 'ログイン'
アドレスとパスワードを入力させます。
controllerの実装
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
を実装しておきます。
これでログインは完了です。
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 self.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
ログインしていなかったらログイン画面に遷移させる。
ログインを実装していても、これをしていないと意味がありません。
sessions_controllerのログアウトアクションは、ログインをしていないと使用できないと思いますのでこれを実装します。
ログインしていなかったらログイン画面に遷移させるrequire_sign_in!
を実装します。
ログイン前のnew, createアクションでは実行させないようにしときましょう。
class SessionsController < ApplicationController
skip_before_action :require_sign_in!, only: [:new, :create]
before_action :set_user, only: [:create]
また、ユーザ登録も、新規でサービスを利用する人が最初に行うオペレーションなので、こちらも同様にログインなしで実行できるようにします。
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の中身を以下に記します。
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が一致しているかでログインしているかを判定する。