これはプログラミングを始めたばかりの独学おじさんがフワフワした理解を整理するために書いたものです。記述や理解に誤りがあれば、ご指摘して頂けると幸いです。
ユーザー登録と認証の基本的な機能
アプリケーションにユーザーを登録させ管理する場合の基本的な機能
新規登録
- 新規登録は新規にユーザーオブジェクトを作成する
- ユーザーオブジェクトの作成に成功と同時にログイン処理へ
ログイン
- ユーザーにセッションを張る
- セッションはユーザー固有の一意の値を渡す
# 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
Rails.application.routes.draw do
resources :users
root "top#index"
end
新規登録
- ユーザーの新規登録は
users#new
で行うがrouteは/signup
とする - 登録フォームでメールアドレスとパスワードを受け取る
- パスワードは暗号化したものをDBに保存する
- オブジェクトの保存に成功→セッションを貼る
routeを設定
resources :users
match '/signup', to: 'users#new', via: 'get'
HTTPメソッド | URL | 名前付きルート | アクション | 用途 |
---|---|---|---|---|
GET | /signup | signup_path | users#new | 新規登録フォーム |
登録フォームの作成
<%= 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
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
bcryptおよびhas_secure_passwordメソッドによるパスワード暗号化の実装はモデルにメソッドを追加するだけ。
class User < ActiveRecord::Base
has_secure_password
end
コントローラーを編集
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: "[email protected]", password: "foo", password_confirmation: "foo")
=> #<User id: 6, name: "taro", email: "[email protected]", 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: "[email protected]", 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を設定
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: ルートパス
で作成する
<%= 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="✓" /><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>
ログイン処理
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
ログアウト
- セッションを削除
- トップページにリダイレクトする
def destroy
reset_session
redirect_to root_path
end
セッションに応じた処理とか
ログインの有無やユーザーに応じて処理を振り分ける
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)属性を与える
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
ログイン処理を担当するコントローラーの実装
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