Railsでgemなしでログイン機能を実装

# はじめに
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にも同様の値を保持させ、
ログインしているかどうかを判別するのに使用します。

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

続いてモデルの作成です。

```ruby:user.rb
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の実装
ログインユーザの作成画面を作ります。

```slim:users/new.html.slim
= 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の実装
```ruby:users_controller.rb
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`に暗号化されて登録されます。

# ログイン機能の実装
ユーザ登録が済んだら、ログイン機能の作成を行います。

## ルーティング設定
```ruby:routes.rb
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の作成
```slim:sessions/new.html.slim
= form_for :session, url: login_path do |f|
  = f.text_field :mail
  = f.password_field :password

  = f.submit 'ログイン'
```

アドレスとパスワードを入力させます。

## controllerの実装
```ruby:sessions_controller.rb
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アクションで検証
```ruby
  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`を実装しておきます。
これでログインは完了です。

```ruby:application_controller.rb
  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
```

```ruby:user.rb
    def self.new_remember_token
      SecureRandom.urlsafe_base64
    end

    def self.encrypt(token)
      Digest::SHA256.hexdigest(token.to_s)
    end
```

### 3.ログアウトの処理
```ruby:sesions_controller.rb
  def destroy
    sign_out
    redirect_to login_path
  end
```

`sign_out`でcookieの中身のremember_tokenを削除します。

```ruby:application_controller.rb
  def sign_out
    cookies.delete(:user_remember_token)
  end
```

# ログインしていなかったらログイン画面に遷移させる。
ログインを実装していても、これをしていないと意味がありません。
sessions_controllerのログアウトアクションは、ログインをしていないと使用できないと思いますのでこれを実装します。
ログインしていなかったらログイン画面に遷移させる`require_sign_in!`を実装します。
ログイン前のnew, createアクションでは実行させないようにしときましょう。

```ruby:sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :require_sign_in!, only: [:new, :create]
  before_action :set_user, only: [:create]
```

また、ユーザ登録も、新規でサービスを利用する人が最初に行うオペレーションなので、こちらも同様にログインなしで実行できるようにします。  

```ruby:users_controller.rb
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の中身を以下に記します。

```ruby:application_controller.rb
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`メソッドを定義します。

```ruby
  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が一致しているかでログインしているかを判定する。