Edited at

メールアドレスとパスワードによる基本的なユーザー認証

More than 3 years have passed since last update.

これはプログラミングを始めたばかりの独学おじさんがフワフワした理解を整理するために書いたものです。記述や理解に誤りがあれば、ご指摘して頂けると幸いです。


ユーザー登録と認証の基本的な機能

アプリケーションにユーザーを登録させ管理する場合の基本的な機能


新規登録


  • 新規登録は新規にユーザーオブジェクトを作成する

  • ユーザーオブジェクトの作成に成功と同時にログイン処理へ


ログイン


  • ユーザーにセッションを張る

  • セッションはユーザー固有の一意の値を渡す


セッション操作の基本

# user.idを元にセッションを渡す

session[:user_id] = user.id
# セッションを元にユーザーオブジェクトを取り出す
user = User.find_by(id: session[:user_id])
# (コメント欄より)idで検索するのでfind_byよりfindを使う
user = User.find(session[:user_id])
# (コメント欄より)User.find()を使うとレコードが存在しない場合は例外が発生する
# find_by_idまたはfind_by(id: )とすると例外ではなくnilを返すことができる
# 状況によって適切に使い分ける
user = User.find_by_id(session[:user_id])

(追記: 2015/06/12 10:54:38) findメソッドの扱いについてご指摘を頂いたのでコンソールで確認してみた。ご指摘ありがとうございます。

rails c

# 存在しないレコードの返り値を検証
# find_by(id: )
User.find_by(id: 10)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 10]]
=> nil

# find_by_id
User.find_by_id(10)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 10]]
=> nil

# find
User.find(10)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 10]]
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=10


ログアウト


  • セッションを削除する


セッションの削除

# 特定のキー単位で破棄する

session[:user_id] = nil
# 全てのセッションを破棄する
reset_session


退会


  • ユーザーのセッションを削除

  • ユーザーオブジェクト削除


    • 場合によってアソシエーションでユーザーに関連付けているオブジェクトも削除




メールアドレスとパスワードによる基本的な実装


準備

$ rails new sample_auth

# $ rails g scaffold user name:string email:string password_digest:string
# string型は省略可能(コメント欄より)
$ rails g scaffold user name email password_digest
$ rails g controller top
$ rake db:migrate


route.rb

Rails.application.routes.draw do

resources :users
root "top#index"
end


新規登録


  • ユーザーの新規登録はusers#newで行うがrouteは/signupとする

  • 登録フォームでメールアドレスとパスワードを受け取る

  • パスワードは暗号化したものをDBに保存する

  • オブジェクトの保存に成功→セッションを貼る

routeを設定


route.rb

resources :users

match '/signup', to: 'users#new', via: 'get'

HTTPメソッド
URL
名前付きルート
アクション
用途

GET
/signup
signup_path
users#new
新規登録フォーム

登録フォームの作成


viesw/users/_form.html.erb

<%= form_for(@user) do |f| %>

<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :email %><br>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %><br>
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

Userモデルが保持する属性はpassword_digestだが、has_secure_passwordメソッドによってpasswordとpassword_confirmation属性が作られる。


暗号化したパスワードをDBに保存する

登録フォームから受け取った値をDBに保存するが、パスワードは暗号化したものを保存する。受け取ったパスワードを暗号化するためにbcryptという文字列を非可逆の暗号にするライブラリを使う。bcryptを使うことによって、ActiveModelに含まれるhas_secure_passwordメソッドが使えるようになる。

has_secure_passwordを使うためには


  • password_digest属性を追加

  • Gemfileにbcryptを追加

has_secure_passwordがやってくれること


  • モデルにpassword属性とpassword_confirmation属性の追加

  • それら属性のvalidation(存在性とそれら属性値の一致を検証)

  • authenticateメソッドの追加


    • 暗号化されてないパスワードとpassword_digest属性値の一致を検証



参考 : Rails - has_secure_passwordを読む - Qiita


Gemfile

# Use ActiveModel has_secure_password

gem 'bcrypt', '~> 3.1.7'

bcryptおよびhas_secure_passwordメソッドによるパスワード暗号化の実装はモデルにメソッドを追加するだけ。


models/user.rb

class User < ActiveRecord::Base

has_secure_password
end

コントローラーを編集


controller/users_controller.rb

class UsersController < ApplicationController

def new
@user = User.new
end

def create
@user = User.new(user_params)

if @user.save
session[:user_id] = user.id
redirect_to @user, notice:"ユーザー登録に成功"
else
render :new
end
end

private

def user_params
params.require(:user).permit(:name, :email, :password, :password_confirm)
end
end


コンソールで確認してみる

$ rails c

# ユーザーの新規作成、保存
user = User.create(name: "taro", email: "taro@example.com", password: "foo", password_confirmation: "foo")
=> #<User id: 6, name: "taro", email: "taro@example.com", password_digest: "$2a$10$WOeeCsOGGKkvWLfFdSLB/OBfPZPOzLrAaYH0Mt2H0Oj...", created_at: "2015-06-11 09:08:20", updated_at: "2015-06-11 09:08:20">

# authenticateメソッド(有効なパスワード)
user.authenticate("foo")
=> #<User id: 6, name: "taro", email: "taro@example.com", password_digest: "$2a$10$WOeeCsOGGKkvWLfFdSLB/OBfPZPOzLrAaYH0Mt2H0Oj...", created_at: "2015-06-11 09:08:20", updated_at: "2015-06-11 09:08:20">

# authenticateメソッド(無効なパスワード)
user.authenticate("bar")
=> false


ログイン


  • ログインフォームはメールアドレスとパスワードを受け取る

  • 受け取ったメールアドレスでユーザーオブジェクトを検索

  • authenticateメソッドでユーザーオブジェクトのpassword_digest属性と受け取ったパスワードを比較し、ユーザーオブジェクトが一致したらログイン(セッションを貼る)

  • セッションはsessions_controllerを用意し、RESTなリソースとして扱う

セッションリソースは以下のように扱う

HTTPメソッド
URL
名前付きルート
アクション
用途

GET
/login
signin_path
new
ログインフォーム

POST
/sessions
sessions_path
create
セッションを作成(ログイン)

DELETE
/logout
signout_path
destroy
セッションを削除する (ログアウト)

sessions_controllerを作成

$ rails g controller sessions

routeを設定


route.rb

resource :sessions, only: [:new, :create, :destroy]

match '/login', to: 'sessions#new', via: 'get'
match '/logout', to: 'sessions#destroy', via: 'delete'

ログインフォームを作成する。セッションコントローラーにはモデルがないので、form_forヘルパーでフォームを作成する場合は、form_for 入力要素の名前, url: ルートパス で作成する


views/sessions/new.html.erb

<%= form_for :session, url: sessions_path do |f| %>

<div class="field">
<%= f.label :email %><br>
<%= f.text_field :email %>
</div>
<div class="field">
<%= f.label :password %><br>
<%= f.password_field :password %>
</div>

<div class="actions">
<%= f.submit %>
</div>
<% end %>


生成されるHTML

<form action="/sessions" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="&#x2713;" /><input type="hidden" name="authenticity_token" value="+3XOu8Hh5qy2mm3Do8/Fc98yhphkjRd8Fx9QGZQ67YjdMlYXYtx0+UFeKpdIVSHQ8vXlt+7/csMzE51YGB0uwQ==" />

<div class="field">
<label for="session_email">Email</label><br>
<input type="text" name="session[email]" id="session_email" />
</div>
<div class="field">
<label for="session_password">Password</label><br>
<input type="password" name="session[password]" id="session_password" />
</div>

<div class="actions">
<input type="submit" name="commit" value="Save Session" />
</div>

</form>

ログイン処理


controller/sessions_controller.rb

def create

# メールアドレスでユーザーを検索
user = User.find_by(email: params[:session][:email])
# パスワードの一致を検証
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
redirect_to user, notice: 'ログイン成功'
else
render :new
end
end


ログアウト


  • セッションを削除

  • トップページにリダイレクトする


controller/sessions_controller.rb

def destroy

reset_session
redirect_to root_path
end


セッションに応じた処理とか

ログインの有無やユーザーに応じて処理を振り分ける


controller/application_controller.rb

class ApplicationController < ActionController::Base

# メソッドをヘルパーメソッドとして登録
helper_method :logged_in?, :current_user

# ログインの有無を得る
def logged_in?
!!session[:user_id]
end

# ログインしているユーザーオブジェクトを得る
def current_user
return unless session[:user_id]
@current_user ||= User.find(session[:user_id])
end
end


ログイン有無に応じてビューを変更する。ログインしていれば、ログインユーザーの名前ログアウト。未ログインなら新規登録ログインのリンクを表示する

<% if logged_in?  %>

<div>
<%= current_user.name %> | <%= link_to "ログアウト", logout_path, method: :delete, data: { confirm: "ログアウト" } %>
</div>
<% else %>
<div>
<%= link_to "新規登録", signup_path %> | <%= link_to "ログイン", login_path %>
</div>
<% end %>


暗号化したtokenでセッション管理するログイン実装

Ruby on Railsチュートリアルで実装した方法


  • 新規登録やログイン時に暗号化されたtokenを生成する

  • 暗号化されたtokenをDBに保存 + セッションを貼る

  • ログイン状態やユーザーの状態管理はセッションのtokenとDBのtokenを擦り合わせて判断する

具体的な実装は以下を参考

(追記: 2015/06/22 16:06:23)RSpecの勉強がてら再びRailsチュートリアルをやったので、ついでにメモ程度の追記をする。

Userモデルにトークン(remember_token)属性を与える


migration

class AddRememberTokenToUsers < ActiveRecord::Migration

def change
add_column :users, :remember_token, :string
add_index :users, :remember_token
end
end

セッション管理のためにユーザーに渡すセッションをユーザーIDではなくランダムで長く一意性を確保できる文字列を渡す。ここではUser#createが実行されるたびにトークンが発行される実装を行う。

class User < ActiveRecord::Base

# User#createが実行されるたびにcreate_remember_tokenが実行される
before_coreate :create_remember_token

# SecureRandomはRuby標準ライブラリで一意性を確保できるランダムな文字列を生成する
def self.new_remember_token
SecureRandom.urlsafe_base64
end

# new_remember_tokenで生成された文字列をハッシュ化する
def self.encrypt(token)
Digest::SHA1.hexdigest(token.to_s)
end

private

# User#createが実行されるたびに、上記の処理で生成されたハッシュを発行する
def create_remember_token
self.remember_token = User.encrypt(User.new_remember_token)
end
end

発行されたトークンを使ってセッションを貼りログインする処理。

module SessionsHelper

# トークンを新規作成
# クライアントにハッシュ化していないremember_tokenを渡し、セッションを貼る
# Userモデルにハッシュ化したremember_tokenを保存する
# ログインユーザー(current_user)とする
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end

# sing_inメソッドで利用する
# サインインしたユーザーをcurrent_userとして管理する
def current_user=(user)
@current_user = user
end

# current_userを参照するメソッド
# クライアントに保存されているremember_tokenを参照
# ハッシュ化し、それを使ってユーザーを検索し、該当ユーザーを返す
def current_user
remember_token = User.encrypt(cookies[:remember_token])
@current_user ||= User.find_by(remember_token: remember_token)
end
end

ログイン処理を担当するコントローラーの実装


controller/sessions_controller

  def create

user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_to user
else
flash.now[:error] = "Invalid email/password combination "
render 'new'
end
end