【Ruby on Rails】Deviseで認証機能を実装する方法

こんにちは！あっきー[(@IwswAkht)](https://twitter.com/IwswAkht)です！

Ruby on Railsの Gemである「Devise」を使った認証機能の実装方法を書きました。
すでに多くの記事もあるし、関わった開発案件でも多々使われている場面はあったのですが、実際に自分でやったことがなかったので忘備録も兼ねた記事になります。

Web系の開発をさせていただくようになってまだ経験は浅いなりに思ったことなのですが、開発現場では言語の知識をめちゃくちゃ覚えておくというよりは以下のことが結構だいじだなと感じています。

・ライブラリを知っていて使えるか
・DBの設計の知識を持っているか
・ある程度のインフラ知識があるか

なので、今後はライブラリなどを積極的に使って見て、その方法や使い勝手などを記事にまとめておきたいと思い本記事を執筆しました。

今後とも継続して知識を蓄積していき、自分や他の誰かの役に立つ記事を増やしていければと。


## Railsプロジェクトの作成

適当な作業フォルダを作成してから Rails tutorialに沿った方法で作成します。
順番に実行していってください。

```Bash:ターミナル
# 1.作業フォルダ作成
$ mkdir dev_rails

# 2.作業フォルダに移動
$ cd dev_rails

# 3.ドキュメントをインストールしない設定
$ printf "install: --no-document \nupdate:  --no-document\n" >> ~/.gemrc

# 4.プロジェクト(sample_app)の作成
$ rails _5.1.6_ new sample_app

# 5.プロジェクトフォルダに移動
$ cd sample_app
```
※rbenvでバージョン管理をした上で作業した方がいいです。以下のURL内の「rbenv で Ruby のインストール」でやり方を書いています。
https://iwasawa-officialweb.com/2019/11/06/rails_secure_shell_app/

## 動作確認

次に簡単に動作確認をしておきましょう。
viewファイルは slimで書きたいので gemを追加した上でインストールします。

```ruby:Gemfile
gem 'slim-rails'
```
以下のコマンドを順番に実行してください。

```bash:ターミナル
# 1.bundle install
$ bundle install --path vendor/bundle

# 2.コントローラとindexファイルの作成
$ rails g controller homes index
```

必要なファイルを作成しましたらルートの設定を行います。

```ruby:config/routes.rb
Rails.application.routes.draw do
  root 'homes#index'
end
```

 `rails s` でサーバーを起動し、`http://localhost:3000/`にアクセスして以下の画面が表示されればOKです！![devise-20200429554.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257800/9d067416-a42c-b71f-fe46-d146434994dd.png)
<hr>
動作の確認が取れましたらいよいよ Deviseを実装してきます。
## Deviseの導入

今回はDeviseで`ログイン/ログアウト/登録`の機能を実装していきます。
まず最初に gemで slimをインストールした手順と同様に Deviseの Gemも追加します。

```ruby:Gemfile
gem 'devise'
```
以下のコマンドを作成します。

```bash:ターミナル
$ bundle install --path vendor/bundle
```

これで Deviseを使うための準備が完了しました。
以下の手順に沿って実装していきます。
### 1. プロジェクトに Deviseを適用させる

以下コマンドを実行してください。
これにより Deviseの設定ファイルが作成されます。

```bash:ターミナル
$ rails g devise:install
```
```:log
Running via Spring preloader in process 51076
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

```

### 2. Modelの作成

Deviseに対応する(ログイン機能用)モデルを作成します。
モデル名は`User`という前提で進めていきます。

以下のコマンドを実行してください。

```bash:ターミナル
$ rails g devise user
```
対応するマイグレーションファイルが以下のように生成されます。
用途に応じてコメントアウトを外します。(今回はデフォルトのままで大丈夫です)

マイグレーションファイルを確認しましたら`$ rails db:migrate`コマンドを実行してマイグレーションします。

```ruby:db/migrate/yyyymmddxxxxxx_devise_create_users.rb
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end
```

また、Userモデルのファイルも作成されていますので確認しておきましょう。
Deviseでは初期でバリデーションがかけられていますが変更したい場合などはこちらのファイルを編集します。

```ruby:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
```
### Controllerの作成

Model(User)に対応するコントローラーを作成します。

以下のコマンドを実行してください。

```bash:ターミナル
$ rails g devise user
```
```:log
Running via Spring preloader in process 51755
      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===============================================================================

```

色々ファイルが作成されてますね。
ファイル名から直感的にわかるかと思うのですが、**登録処理**や**ログイン/ログアウト**に関するコントローラーが作成されまして各々編集しながら各機能に対する処理を記述します。(今回はデフォルトのままで大丈夫です)

また、ログにも出てますが、まだ設定が完了していません。コントローラーに対応するビューの設定をルーティングしていく必要があるみたいですね。

### Routesの設定

今回は`ログイン/ログアウト/登録`なので以下のようにルートファイルを編集します

```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions:      'users/sessions',
    registrations: 'users/registrations',
  }
  root 'homes#index'
end
```
### Viewの作成
残りはビューファイルの作成です。

Deviseの以下のコマンドで必要なファイルを一括で作成できます。

```bash:ターミナル
$ rails g devise:views users
```
```:log
Running via Spring preloader in process 51718
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/users/shared
      create    app/views/users/shared/_error_messages.html.erb
      create    app/views/users/shared/_links.html.erb
      invoke  form_for
      create    app/views/users/confirmations
      create    app/views/users/confirmations/new.html.erb
      create    app/views/users/passwords
      create    app/views/users/passwords/edit.html.erb
      create    app/views/users/passwords/new.html.erb
      create    app/views/users/registrations
      create    app/views/users/registrations/edit.html.erb
      create    app/views/users/registrations/new.html.erb
      create    app/views/users/sessions
      create    app/views/users/sessions/new.html.erb
      create    app/views/users/unlocks
      create    app/views/users/unlocks/new.html.erb
      invoke  erb
      create    app/views/users/mailer
      create    app/views/users/mailer/confirmation_instructions.html.erb
      create    app/views/users/mailer/email_changed.html.erb
      create    app/views/users/mailer/password_change.html.erb
      create    app/views/users/mailer/reset_password_instructions.html.erb
      create    app/views/users/mailer/unlock_instructions.html.erb
user@usernoMacBook-Air sample_app % rails g devise:controllers users

```

最初に動作確認で作ったファイルをログイン後の画面として以下の編集を加えてください。

```ruby:app/views/homes/index.html.slim
h1 ログイン後の画面
= link_to 'ログアウト', destroy_user_session_path, method: :delete
```

ここまで出来ましたら`rails s`でサーバーを起動しまして各画面を確認してみましょう。

`http://localhost:3000/users/sign_up`
![devise-up202004281038.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257800/bf783689-2c4f-bc0e-aeb7-aecdfb9b4666.png)
<hr>
`http://localhost:3000/users/sign_in`
![devise-in202004281037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257800/83f4a1f0-6b4b-95ce-1461-252eafeb0a58.png)
`http://localhost:3000/`
<hr>
![devise-top202004281036.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/257800/f8bea654-440c-3ba6-11cb-a781dd875467.png)
<hr>
適当に`http://localhost:3000/users/sign_up`からテストユーザーを作ってみてください。

## 認証制御

現在の状況ですと`http://localhost:3000/`のURLを直接叩くと認証(ログイン)をしていないのにアクセスすることが可能です。
これはアプリ的に非常によろしくないですね。

なのでログイン後にしかアクセスすることが出来ないよう制御をかけましょう。
Deviseを使った場合は認証後にアクセスしたいビューに対応するコントローラに以下の編集を加えることで完了します。

```ruby:app/controllers/homes_controller.rb
class HomesController < ApplicationController
  before_action :authenticate_user!　# 追加
  def index
  end
end
```

これでログイン前にログイン後の画面にアクセスするとログイン画面にリダイレクトされ、逆にログイン中にログイン画面へアクセスするとトップ画面にリダイレクトさせるようになります。

ほとんどコマンドで、あとは数行の追加で認証機能が実装できてめちゃくちゃ便利ですよね。

## 日本語対応

デフォルトでは英語対応なので日本語対応に変更しましょう。

以下のファイルに変更を加えてください。

```ruby:config/application.rb
require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module SampleApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
    config.i18n.default_locale = :ja　#追加(言語対応)
  end
end
```

続いて`config/locales/`配下に`devise.ja.yml`と`ja.yml`を作成しそれぞれに編集を加えます。

```ruby:config/locales/devise.ja.yml
ja:
  errors:
    messages:
      not_found: "は見つかりませんでした"
#      not_found: "not found"
      already_confirmed: "は既に登録済みです"
#      already_confirmed: "was already confirmed"
      not_locked: "は凍結されていません"
#      not_locked: "was not locked"

  devise:
    failure:
      unauthenticated: 'ログインしてください。'
#      unauthenticated: 'You need to sign in or sign up before continuing.'
      unconfirmed: '本登録を行ってください。'
#      unconfirmed: 'You have to confirm your account before continuing.'
      locked: 'あなたのアカウントは凍結されています。'
#      locked: 'Your account is locked.'
      invalid: 'メールアドレスかパスワードが違います。'
#      invalid: 'Invalid email or password.'
      invalid_token: '認証キーが不正です。'
#      invalid_token: 'Invalid authentication token.'
      timeout: 'セッションがタイムアウトしました。もう一度ログインしてください。'
#      timeout: 'Your session expired, please sign in again to continue.'
      inactive: 'アカウントがアクティベートされていません。'
#      inactive: 'Your account was not activated yet.'
    sessions:
      signed_in: 'ログインしました。'
#      signed_in: 'Signed in successfully.'
      signed_out: 'ログアウトしました。'
#      signed_out: 'Signed out successfully.'
    passwords:
      send_instructions: 'パスワードのリセット方法を数分以内にメールでご連絡します。'
#      send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
      updated: 'パスワードを変更しました。'
#      updated: 'Your password was changed successfully. You are now signed in.'
    confirmations:
      send_instructions: '登録方法を数分以内にメールでご連絡します。'
#      send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
      confirmed: 'アカウントを登録しました。'
#      confirmed: 'Your account was successfully confirmed. You are now signed in.'
    registrations:
      signed_up: 'アカウント登録を受け付けました。確認のメールをお送りします。'
#      signed_up: 'You have signed up successfully. If enabled, a confirmation was sent to your e-mail.'
      updated: 'アカウントを更新しました。'
#      updated: 'You updated your account successfully.'
      destroyed: 'アカウントを削除しました。またのご利用をお待ちしております。'
#      destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
    unlocks:
      send_instructions: 'アカウントの凍結解除方法を数分以内にメールでご連絡します。'
#      send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
      unlocked: 'アカウントを凍結解除しました。'
#      unlocked: 'Your account was successfully unlocked. You are now signed in.'
    mailer:
      confirmation_instructions:
        subject: 'アカウントの登録方法'
#        subject: 'Confirmation instructions'
      reset_password_instructions:
        subject: 'パスワードの再設定'
#        subject: 'Reset password instructions'
      unlock_instructions:
        subject: 'アカウントの凍結解除'
#        subject: 'Unlock Instructions'
```
```ruby:config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        confirmation_sent_at: パスワード確認送信時刻
        confirmation_token: パスワード確認用トークン
        confirmed_at: パスワード確認時刻
        created_at: 作成日
        current_password: 現在のパスワード
        current_sign_in_at: 現在のログイン時刻
        current_sign_in_ip: 現在のログインIPアドレス
        email: Eメール
        encrypted_password: 暗号化パスワード
        failed_attempts: 失敗したログイン試行回数
        last_sign_in_at: 最終ログイン時刻
        last_sign_in_ip: 最終ログインIPアドレス
        locked_at: ロック時刻
        password: パスワード
        password_confirmation: パスワード（確認用）
        remember_created_at: ログイン記憶時刻
        remember_me: ログインを記憶する
        reset_password_sent_at: パスワードリセット送信時刻
        reset_password_token: パスワードリセット用トークン
        sign_in_count: ログイン回数
        unconfirmed_email: 未確認Eメール
        unlock_token: ロック解除用トークン
        updated_at: 更新日
    models:
      user: ユーザ
  devise:
    confirmations:
      confirmed: メールアドレスが確認できました。
      new:
        resend_confirmation_instructions: アカウント確認メール再送
      send_instructions: アカウントの有効化について数分以内にメールでご連絡します。
      send_paranoid_instructions: メールアドレスが登録済みの場合、本人確認用のメールが数分以内に送信されます。
    failure:
      already_authenticated: すでにログインしています。
      inactive: アカウントが有効化されていません。メールに記載された手順にしたがって、アカウントを有効化してください。
      invalid: "%{authentication_keys}またはパスワードが違います。"
      last_attempt: もう一回誤るとアカウントがロックされます。
      locked: アカウントは凍結されています。
      not_found_in_database: "%{authentication_keys}またはパスワードが違います。"
      timeout: セッションがタイムアウトしました。もう一度ログインしてください。
      unauthenticated: アカウント登録もしくはログインしてください。
      unconfirmed: メールアドレスの本人確認が必要です。
    mailer:
      confirmation_instructions:
        action: メールアドレスの確認
        greeting: "%{recipient}様"
        instruction: 以下のリンクをクリックし、メールアドレスの確認手続を完了させてください。
        subject: メールアドレス確認メール
      email_changed:
        greeting: こんにちは、%{recipient}様。
        message: あなたのメール変更（%{email}）のお知らせいたします。
        subject: メール変更完了。
      password_change:
        greeting: "%{recipient}様"
        message: パスワードが再設定されたことを通知します。
        subject: パスワードの変更について
      reset_password_instructions:
        action: パスワード変更
        greeting: "%{recipient}様"
        instruction: パスワード再設定の依頼を受けたため、メールを送信しています。下のリンクからパスワードの再設定ができます。
        instruction_2: パスワード再設定の依頼をしていない場合、このメールを無視してください。
        instruction_3: パスワードの再設定は、上のリンクから新しいパスワードを登録するまで完了しません。
        subject: パスワードの再設定について
      unlock_instructions:
        action: アカウントのロック解除
        greeting: "%{recipient}様"
        instruction: アカウントのロックを解除するには下のリンクをクリックしてください。
        message: ログイン失敗が繰り返されたため、アカウントはロックされています。
        subject: アカウントの凍結解除について
    omniauth_callbacks:
      failure: "%{kind} アカウントによる認証に失敗しました。理由：（%{reason}）"
      success: "%{kind} アカウントによる認証に成功しました。"
    passwords:
      edit:
        change_my_password: パスワードを変更する
        change_your_password: パスワードを変更
        confirm_new_password: 確認用新しいパスワード
        new_password: 新しいパスワード
      new:
        forgot_your_password: パスワードを忘れましたか?
        send_me_reset_password_instructions: パスワードの再設定方法を送信する
      no_token: このページにはアクセスできません。パスワード再設定メールのリンクからアクセスされた場合には、URL をご確認ください。
      send_instructions: パスワードの再設定について数分以内にメールでご連絡いたします。
      send_paranoid_instructions: メールアドレスが登録済みの場合、パスワード再設定用のメールが数分以内に送信されます。
      updated: パスワードが正しく変更されました。
      updated_not_active: パスワードが正しく変更されました。
    registrations:
      destroyed: アカウントを削除しました。またのご利用をお待ちしております。
      edit:
        are_you_sure: 本当によろしいですか?
        cancel_my_account: アカウント削除
        currently_waiting_confirmation_for_email: "%{email} の確認待ち"
        leave_blank_if_you_don_t_want_to_change_it: 空欄のままなら変更しません
        title: "%{resource}編集"
        unhappy: 気に入りません
        update: 更新
        we_need_your_current_password_to_confirm_your_changes: 変更を反映するには現在のパスワードを入力してください
      new:
        sign_up: アカウント登録
      signed_up: アカウント登録が完了しました。
      signed_up_but_inactive: ログインするためには、アカウントを有効化してください。
      signed_up_but_locked: アカウントが凍結されているためログインできません。
      signed_up_but_unconfirmed: 本人確認用のメールを送信しました。メール内のリンクからアカウントを有効化させてください。
      update_needs_confirmation: アカウント情報を変更しました。変更されたメールアドレスの本人確認のため、本人確認用メールより確認処理をおこなってください。
      updated: アカウント情報を変更しました。
      updated_but_not_signed_in: あなたのアカウントは正常に更新されましたが、パスワードが変更されたため、再度ログインしてください。
    sessions:
      already_signed_out: 既にログアウト済みです。
      new:
        sign_in: ログイン
      signed_in: ログインしました。
      signed_out: ログアウトしました。
    shared:
      links:
        back: 戻る
        didn_t_receive_confirmation_instructions: アカウント確認のメールを受け取っていませんか?
        didn_t_receive_unlock_instructions: アカウントの凍結解除方法のメールを受け取っていませんか?
        forgot_your_password: パスワードを忘れましたか?
        sign_in: ログイン
        sign_in_with_provider: "%{provider}でログイン"
        sign_up: アカウント登録
      minimum_password_length: "（%{count}字以上）"
    unlocks:
      new:
        resend_unlock_instructions: アカウントの凍結解除方法を再送する
      send_instructions: アカウントの凍結解除方法を数分以内にメールでご連絡します。
      send_paranoid_instructions: アカウントが見つかった場合、アカウントの凍結解除方法を数分以内にメールでご連絡します。
      unlocked: アカウントを凍結解除しました。
  errors:
    messages:
      already_confirmed: は既に登録済みです。ログインしてください。
      confirmation_period_expired: の期限が切れました。%{period} までに確認する必要があります。 新しくリクエストしてください。
      expired: の有効期限が切れました。新しくリクエストしてください。
      not_found: は見つかりませんでした。
      not_locked: は凍結されていません。
      not_saved:
        one: エラーが発生したため %{resource} は保存されませんでした。
        other: "%{count} 件のエラーが発生したため %{resource} は保存されませんでした。"
```

以上です。

## さいごに

最後まで読んでいただきありがとうございます。

今回は基本的な Deviseの扱い方のみになりますが、次は SNSログインとの連携や devise_invitableでの連携などもやってみようかと思います。

自分のブログでも本記事の内容は投稿していますが、ブログでは技術系以外のことも更新していますので興味があればみていってください。
https://iwasawa-officialweb.com/