write ahead log

ロールフォワード用

Rails5でDeviseを使って認証機能を作る

えらく多機能なので試しながらメモる.

とりあえず使う

とりあえず使ってみる.

認証をするからにはサンプルが欲しいので用意しよう.

サンプルプロジェクトを考える

メモを管理するサービスということにしよう.

コントローラは

  • StaticPages(トップページ管理)
  • Notes(メモの管理)

の2つを用意する.

機能は認証とメモ管理のみ.

プロジェクトを作る

$ rails new notes

Deviseのインストール

gem 'devise'

をGemfileへ書いて

$ bundle install

とりあえずトップページとメモ機能を作る

とりあえずなのでscaffoldで.

$ rails g controller StaticPages
$ rails g scaffold note subject:string body:string
$ rails db:migrate

StaticPagesへはconfig/routes.rbを編集してルートも追加.

Rails.application.routes.draw do
   root to: 'static_pages#index'  #追加
   resources :notes
end

雑にトップページも作っておく.

<!-- app/views/static_pages/index.html.erb  -->
<h1>Top</h1>

ここまででメモ機能は動作する.

Deviseのセットアップ

ここからDeviseを使っていく.

Deviseを使うにはまず以下のコマンドでセットアップする.

$ rails g devise:install

そうすると丁寧に以下のメッセージを出してくれる.

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

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

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

1はメーラの設定, Deviseは認証やパスワードリマインダーにメールを使えるのでそのため. 2はルートページURLの設定, これは認証時の遷移先で必要になるだろう. 3はエラーメッセージなどを表示するタグをviewに入れてくれということ. 4はカスタムのviewが欲しい時に使えという事.

Deviseはデフォルトでviewを用意するのでカスタムする時には4のコマンドでまずはviewのファイルを生成してやらないとならない.
(viewをカスタムしない場面もあまりなさそうなものだけど)

2のルートページの設定は上述で行ったので, 1, 3を行います.

カスタムviewは後回し.

1. メーラの設定

# config/environments/development.rb
# 追加
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

3.エラーメッセージタグのviewへの追加

app/views/layout/application.html.erb

<!-- ここから -->
<div class="notice">
  <%= notice %>
</div>
<div class="alert">
  <%= alert %>
</div>
<!-- ここまで -->
<%= yield %>

認証に使うモデルの作成

認証するためのアカウント情報を保持するためのモデルを作成します.

作成にはDeviseが用意しているコマンドを使います.

$ rails g devise User

作成されたファイルを少し覗いてみます.

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

deviseの利用するモジュールがわらわら書かれています(後述)

マイグレーションファイルも見てみます.

# 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

すごいわらわら出来てます...

コメントアウトされているものをアンコメントするとConfirmableなどのモジュールも利用できそうな気配.

とりあえずデフォルトの認証機能さえ使えれば良いのでマイグレーション.

$ rails db:migrate

サインインとサインアウトのリンクをつける

サインインとサインアウトの画面へのリンクをトップページに作りたいので, routesを確認してみます.

imai@IMAI-PC:~/lab/projects/notes$ rails routes
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
           user_password PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
                         POST   /users/password(.:format)      devise/passwords#create
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                         POST   /users(.:format)               devise/registrations#create
                         [中略]

とりあえずusersの部分だけ見てもたくさん生成されています.

とはいえリンクに使えそうなものは以下でしょう.

  • new_user_registration
  • new_user_session
  • destroy_user_session

これらを使ってリンクを用意します.

トップページからのリンクを作る

ユーザ登録(sign up)とログイン(sign in)は必要でしょう.

トップページ(app/views/static_pages/index.html.erb)にリンクを加えます.

<h1>Top</h1>

<%= link_to 'sign in', new_user_session_path %>

<%= link_to 'sign up', new_user_registration_path %>

ログアウトのリンクを作る

ログインしたらログアウトのリンクも欲しいのでレイアウト(app/views/layouts/application.html.erb)へ加えます.

  <body>
  <!-- ここから -->
    <div class="session-control-box">
      <% if signed_in? %>
        <%= link_to 'sign out', destroy_user_session_path, method: :delete %>
      <% end %>
    </div>
  <!-- ここまで -->

signed_in?はdeviseが用意してくれるヘルパーメソッドです.

その名の通りログイン中だったらtrueを返します.

ログイン後のリダイレクトを設定する

ログイン後にはメモの一覧へ移動してほしいのでヘルパーメソッドの利用を追加します.

app/controllers/application_controller.rbに

  • after_sign_in_path_for
  • after_sign_out_path_for

の利用を追加します.

名前の通りこのメソッドの戻り値のパスへリダイレクトされます.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def after_sign_in_path_for(resource)
    notes_path
  end

  def after_sign_out_path_for(resource)
    root_path
  end
end

これでログイン後とログアウト後の挙動を変更できます.

ログイン前はアクセスできないようにする

リダイレクトされてもログイン前にメモ管理のエリアにアクセスできてしまっては本末転倒なので, 認証機能を追加します.

class NotesController < ApplicationController
  before_action :authenticate_user!  #追加
  before_action :set_note, only: [:show, :edit, :update, :destroy]

authenticate_[モデル名]メソッドがdeviseで用意されているのでこれを使うだけです.

未認証の場合はログイン画面へリダイレクトされます.

ログインした人間のノートだけ参照できるようにする

このままではログインさえすれば誰のメモでも見れてしまうので, メモに所有者情報を持たせてログインしているユーザのメモだけが見れるように変更します.

まずはmigrationでnoteへuserへの外部キーを追加します.

$ rails g migration AddUserToNotes user:references
$ rails db:migrate

モデルへアソシエーションもつけておきます.

ユーザ側.

# app/models/user.rb
class User < ApplicationRecord
  has_many :notes

メモ側.

# app/models/note.rb
class Note < ApplicationRecord
  belongs_to :user
end

次にscaffoldで作ったコントローラをちょっと変更.

  def index
    #[変更]
    #@notes = Note.all
    @notes = current_user.notes
  end
    [中略]

  def new
    #[変更]
    #@note = Note.new
    @note = current_user.notes.build
  end
    [中略]

  def create
    #[変更]
    #@note = Note.new(note_params)
    @note = current_user.notes.build(note_params)
    [中略]

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_note
      #[変更]
      #@note = Note.find(params[:id])
      @note = current_user.notes.find_by(id: params[:id])
      redirect_to notes_url if @note.nil?
    end

current_[モデル名]ヘルパーメソッドがdeviseによって用意されるのでログイン中のユーザを取得するのは簡単です.

ここまでやると普通にアプリケーションっぽくなります.

i18nに対応する

deviseそのままでは英語メッセージが表示されるのでI18N対応してやる.

なんとそのままの名前のgemがあった.

tigrish/devise-i18n - GitHub

余談だがこの方, kaminariとか色々なgemのi18n対応をやってるみたいで地味にすごい.

Gemfileに追記して更新する.

# 追記
gem 'devise-i18n'
$ bundle install

デフォルトロケールを設定していない場合はこちらも設定.

# config/application.rb

# 以下を記述
config.i18n.default_locale = :ja

これだけでメッセージが日本語化される.ありがたい.

公式にも一応色々記載があるので込み入ったことをしたい場合は読むとよいかも.

専用のwikiもある.

ViewとControllerをカスタマイズする

ちょっと試すぐらいならデフォルトでも良いけど, 流石にこのままのviewでは実用に耐えない.

モデルもちょっといじって, 登録時にユーザ名も登録するようにしてみる.

Userモデルの変更

migrationでユーザ名を追加する.

$ rails g migration AddNameToUsers name:string
$ rails db:migrate

deviseのviewの用意

deviseでviewをカスタマイズするにはまず以下のコマンドでビューを生成する.

$ rails g devise:views users(モデル名)

するとdeviseで使うビューが用意される.

これをカスタマイズしていく.

deviseのcontrollerの用意

controllerもviewと同様で, 下記のコマンドで生成する.

$ rails g devise:controllers users

親切にroutes.rbを変更しろと教えてくれる.

      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

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

生成されたコントローラがapp/controllers/users/に用意されるが, これらは全てコメントアウトされている.

なのでこれをベースに使うものをアンコメントしていかないとならない.

各コントローラと機能の対応は多分以下の感じじゃないかな.

コントローラ名 機能
confirmations_controller.rb 登録後のメールでの確認
passwords_controller.rb パスワードリマインダー
registrations_controller.rb ユーザ登録
sessions_controller.rb セッション(ログイン/ログアウト)
omni_auth_callbacks_controller.rb omni_authによるOAuth制御のコールバック
unlocks_controller.rb ログインに一定回数失敗したらアカウントロックするアレ

とりあえず

  • ユーザ登録
  • セッション管理

だけは使いたいので以下の2つのファイルの中身をアンコメントする.

  • registrations_controller
  • sessions_controller

routesを変更

コンソールで出た指示通りに.

  #devise_for :users
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

ここまででコントローラとビューをカスタムする前と同じ挙動になる.

viewの変更

app/views/users/registrations/new.html.erbを変更して名前欄を追加する.

<!-- ここから -->
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
<!-- ここまで -->

  <div class="field">

ついでにログイン中のユーザ名を見えるようにしておく.

<!-- app/views/layout/application.html.erbへ追加 -->
        <p><%= current_user.name %></p>

controllerの変更

strong parametersを追加するだけですが.

  def configure_sign_up_params
    #devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end

公式にstrong parametersに関する記載があったので, 困ったら読めば良いかな.

localeファイルの編集

普通にjaのymlを用意してActiveRecordの部分に書いた.

# config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: ユーザ名

これだけでうまく動作します.素敵ですね.

testをしたい

公式にも章が組まれている.

ControllerのテストもIntegrationテストもどちらでもIntegrationHelpersをincludeすると

  • sign_in
  • sign_out

の両メソッドが利用できるので, これで困ることはほぼないと思う.

# test/controllers/note_controller_test.rb
class NotesControllerTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  setup do
    sign_in users(:yamada)
  end

ちょっと調べる必要があったのがFixturesの書き方で, 以下の様に書いた.

# test/fixtures/users.yml
yamada:
    email: yamada@example.com
    encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>

モジュールの一覧

deviseには以下の10個のモジュールがあり, それぞれ用途に応じて使い分ける事ができる.

モジュール名 機能 デフォルト
database_authenticatable DBに保存するパスワードの暗号化 有効
registerable サインアップ処理 有効
recoverable パスワードリセット機能 有効
rememberable クッキーにログイン情報を保持する 有効
trackable サインイン回数・時刻・IPアドレスを保存 有効
validatable メールアドレスとパスワードでのバリデーション 有効
confirmable メール送信による登録確認
lockable 一定回数ログインに失敗した際のアカウントロック
timeoutable 一定時間でセッションを削除する
omniauthable OmniAuthのサポート

ちょっとすぐに全部は試せそうにない.心折れた.

とはいえこれだけでかなり使えそうです.

参考

Documentation for plataformatec/devise (master)