LoginSignup
58

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-06-11

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

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

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

新規登録

  • 新規登録は新規にユーザーオブジェクトを作成する
  • ユーザーオブジェクトの作成に成功と同時にログイン処理へ

ログイン

  • ユーザーにセッションを張る
  • セッションはユーザー固有の一意の値を渡す
セッション操作の基本
# 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

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
58