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


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

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

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

## 新規登録

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

## ログイン

* ユーザーにセッションを張る
* セッションはユーザー固有の一意の値を渡す

```ruby:セッション操作の基本
# 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メソッドの扱いについてご指摘を頂いたのでコンソールで確認してみた。ご指摘ありがとうございます。

```bash
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
```







## ログアウト

* セッションを削除する

```ruby:セッションの削除
# 特定のキー単位で破棄する
session[:user_id] = nil
# 全てのセッションを破棄する
reset_session
```

## 退会

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

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

```bash:準備
$ 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
```

```ruby:route.rb
Rails.application.routes.draw do
  resources :users
  root "top#index"
end
```


## 新規登録

* ユーザーの新規登録は`users#new`で行うがrouteは`/signup`とする
* 登録フォームでメールアドレスとパスワードを受け取る
* パスワードは暗号化したものをDBに保存する
* オブジェクトの保存に成功→セッションを貼る

routeを設定

```ruby:route.rb
resources :users
match '/signup', to: 'users#new', via: 'get'
```


| HTTPメソッド | URL     | 名前付きルート | アクション | 用途             |
|--------------|---------|----------------|------------|------------------|
| GET          | /signup | signup_path    | users#new   | 新規登録フォーム |


登録フォームの作成

```html: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](http://qiita.com/dblN/items/9da63698199f4e6135ae)

```ruby:Gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
```

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

```ruby:models/user.rb
class User < ActiveRecord::Base
  has_secure_password
end
```

コントローラーを編集

```ruby: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
```


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

```bash
$ 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を設定

```ruby: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: ルートパス`  で作成する

```html: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


```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
```


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

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

```ruby: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
```

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

```html
<% 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を擦り合わせて判断する


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

* [第8章 サインイン、サインアウト | Rails チュートリアル](http://railstutorial.jp/chapters/sign-in-sign-out?version=4.0#top)
* [[Rails] セッション管理をベタで実装してみる - Qiita](http://qiita.com/kidach1/items/ebdb4b29336955903029)



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

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

```ruby:migration
class AddRememberTokenToUsers < ActiveRecord::Migration
  def change
    add_column :users, :remember_token, :string
    add_index :users, :remember_token
  end
end
```


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

```ruby
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
```


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

```ruby
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
```

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

```ruby: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
```