No day younger than today

RubyとかRailsとか蒙古タンメンとか

【Rails】deviseを使ってユーザー認証を実装する

こんにちは!

フィヨルドブートキャンプでWebエンジニアを目指して学習中のふーがです。

Railsの「devise」というGemは、コマンド1つで認証機能を手軽に作成できる人気のGemです。 主にユーザー認証に使われることが多いようです。

heartcombo/devise: Flexible authentication solution for Rails with Warden.

今回、deviseを使用して簡単なユーザ認証機能を実装するプラクティスを完了したので、そこで得たdeviseの使い方や気づきをまとめてみました。

2021年4月現在の内容で作成しています。内容の間違いや、認識の誤りがあればぜひ(やさしめに)ご指摘いただけますと幸いです。

環境

deviseとは何か

冒頭でも触れたとおり、簡単に認証機能を実装することができるGemです。

具体的には、

  • 登録
  • 退会
  • ログイン
  • ログアウト
  • 情報編集
  • パスワードリセット

といった機能が追加されます。

どれも一から自前で実装しようと思うと手間が掛かりますし、考慮すべき事項(セキュリティ面も含めて)も多いので、大変です。

が、deviseを使うとそれらをまるっとさくっと解決してくれるので非常に便利です。

驚くほど簡単に認証機能を実装することができます。

とりあえず、使ってみる

まずは実際どんな風に導入するのか、導入するとどうなるのかを見てみます。

deviseのインストール

Gemfileにdeviseを追加します。

# Gemfile
gem 'devise'

Gemfileを更新したら、bundlerを実行します。

$ bundle install

次に、deviseのジェネレーターを実行します。

$ bin/rails generate devise:install

ジェネレーターを実行するといくつかのオプションが表示されます。

とりあえずこの段階では、deviseのMailerで使用するデフォルトURLのみ設定しておきます。

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

ここまでで、インストールと最小限の設定は完了です。

Modelの作成

続いて、deviseで扱うModelを作成していきます。

オーソドックスにユーザーモデルを作る前提で進めていきます。

$ bin/rails generate devise User

するといろいろとファイルが作成されるのですが、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

ここで定義されているのは、deviseのモジュール(機能)のうちどれを使うか?です。

それぞれのモジュールの役割は、次の表のとおりです。

モジュール 概要
database_authenticatable サインイン時にユーザーの正当性を検証するためにパスワードをハッシュ化してDBに登録します。認証方法としてはPOSTリクエストかHTTP Basic認証が使えます。
registerable 登録処理を通してユーザーをサインアップします。また、ユーザーが自身のアカウントを編集したり削除できるようにします。
recoverable パスワードをリセットし、それを通知します。
rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除します。
trackable サインイン回数や、サインイン時間、IPアドレスを記録します。
validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます。
confirmable メールに記載されているURLをクリックして本登録を完了する、といったよくある登録方式を提供します。また、サインイン中にアカウントが認証済みかどうかを検証します。
lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります。
timeoutable 一定時間活動していないアカウントのセッションを破棄します。
omniauthable intridea/omniauthをサポートします。TwitterFacebookなどの認証を追加したい場合はこれを使用します。

出典:[Rails] deviseの使い方(rails6版) - Qiita

必要なモジュールを設定して、マイグレーションを実行します。

$ bin/rails db:migrate

ちゃんとユーザーが作成できるか試してみます。

$ bin/rails c
irb(main):001:0> User.create
=> #<User id: nil, email: "", created_at: nil, updated_at: nil>

ユーザーが追加できるようになりました。

現状のアプリケーションがどうなっているか確認してみます。

まずはサーバーを起動します。

$ bin/rails s

試しに、http://localhost:3000/users/sign_upにアクセスしてみます。

すると、登録画面が表示されます。

f:id:fuga__ch:20210403163027p:plain

何も入力せずに「Sign up」をクリックすると…

f:id:fuga__ch:20210403163031p:plain

バリデーションもきっちりされています。

この段階で、「登録」「ログイン」「ログアウト」「パスワードリセット」「ユーザー情報編集」「ユーザー削除」が全て利用できるようになっています。

※パスワードリセットは機能として実装はされていますが、メール送信は別途実装する必要があります。

こんな簡単な手順でこれだけの機能が実装できてしまうdeviseすごい…!

ヘルパーメソッドを使ってヘッダーメニューを実装する

deviseはコントローラやviewで使用するヘルパーを自動生成してくれます。

メソッド 機能
authenticate_user! before_actionで指定することで、ユーザー認証機能を追加します。
user_signed_in? ユーザーがサインインしているかどうかを検証します。
current_user 現在サインインしているユーザに対して利用できます。current_user.emailで、現在のユーザーの登録メールアドレスが取得できます。

これらを組み合わせて、ヘッダーメニューを作ってみます。

# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Title</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <!--ここから追加部分-->
    <p>
      <% if user_signed_in? %>
        Logged in as <strong><%= current_user.email %></strong>.
        <%= link_to 'Edit profile', edit_user_registration_path %> |
        <%= link_to 'Logout', destroy_user_session_path, method: :delete %>
      <% else %>
        <%= link_to 'Sign up', new_user_registration_path %> |
        <%= link_to 'Login', new_user_session_path %>
      <% end %>
    </p>
    <!--追加ここまで-->
    <%= yield %>
  </body>
</html>

これで、全てのページの上部にヘッダーメニューが表示されます。

f:id:fuga__ch:20210403163035p:plain

f:id:fuga__ch:20210403163038p:plain

大切なのはbodyタグの後のpタグの内容です。

最初の条件分岐で、サインインしているかどうかを判定して、メニューを出し分けています。

<% if user_signed_in? %>

user_signed_in? は、サインインしているならtrueを、していなければfalseを返します。

つまり、現在アクセスしているクライアントが、ログイン中ならプロフィール編集とログアウトへのリンクを、ログイン中でなければ登録とログインへのリンクを出力しています。

ちなみに、link_toメソッドのリンク先は、コンソールでrails routesを実行するか、railsサーバーを起動した状態で/rails/info/routesにアクセスすればわかります。

他にもいろいろできることがあります

ここまでで基本的なユーザー認証の機能は実装できていますが、他にもまだまだできることがあります。

ほんの一部ですが、実際に僕が実装した部分を紹介します。

ユーザー認証を実装する

deviseは前述のとおり、ヘルパーメソッドを使って簡単にユーザー認証を実装することができます。

# app/controllers/application/_controller.rb
class ApplicationController < ActionController::Base
    before_action :authenticate_user!
end

これだけでユーザー認証が実装されます。

具体的には、コントローラーに処理がわたるとはじめにauthenticate_userが実行され、ログインしているかどうかのチェックが行われます。

そして、ログインしていなければrootにリダイレクトします。

上の例は、deviseでUserモデルを作成していることを前提としています。

もし違うモデル名で作成している場合には、_userの部分をモデル名に置き換えます。

✍🏻 Tips

僕は当初、BookモデルとUserモデルという2つのモデルにそれぞれユーザー認証を付けたかったので、それぞれのコントローラーにユーザー認証のヘルパーを書いていました。

しかし、セキュリティ的には、安全側を実装しておいて必要に応じて開放するのが基本だとレビューしてもらいました。 つまり、application_controllerにユーザー認証を実装しておいて、ユーザー認証が不要な場合のみ、skip_before_actionで解除するのが安全だということです。

確かにその方が、ユーザー認証の実装漏れによるセキュリティリスクは低くなるし、同じコードを何箇所にも書かなくて済むなぁと思いました。

独自のviewを用意する

✍🏻Tips

ここで紹介する方法は、devise-i18nで国際化したviewを使わない場合にのみ実行するのが良いです。

devise-i18nを使う場合は(リンク)のやり方を参照してください。

localeファイルとの生合成が取れなくなって翻訳がおかしなことになります。

deviseはジェネレーターを実行した時に自動的にviewも生成してくれますが、デフォルトは味気ないですし、編集する必要が出てくる場合がほとんどだと思います。

しかし、どこを探してもviewファイルはありません。

そんな時のために、独自のview用のジェネレーターが用意されています。

$ rails generate devise:views

ジェネレーターを実行すると、viewファイル一式がapp/views/devise配下に生成されます。

f:id:fuga__ch:20210403163048p:plain

deviseでは基本的に、1つのviewファイルを使いまわして複数のModelにも対応しています。

しかし、Modelごとにviewを変えたいという場合もあると思います(僕はまだ出くわしていませんが)。

そういう時用に、Modelごとのviewファイルを作成するジェネレーターも用意されています。

$ rails g devise:views User

User の部分は、それぞれのModel名に置き換えて使用します。

ジェネレーターを実行すると、Model名に対応したviewファイルが生成されます。

f:id:fuga__ch:20210403163052p:plain

今度は、app/views/users配下にviewファイルが作成されました。

これで変更したいviewファイルを編集すれば一件落着…というわけにはいきません。

Modelに応じたviewファイルに対応させるには、deviseの設定を変更する必要があります。

# config/initializers/devise.rb
config.scoped_views = true

config.scoped_viewsで検索すると、デフォルトでは falseになっているのがわかります。

その次の行に上記の設定を追記してやれば、Modelごとのviewファイルを有効化することができます。

独自のカラムを追加する

Strong Parameters

独自のviewに新しいカラムを追加する場合、そのままでは受け付けてもらえません。

理由は、deviseデフォルトのStrong Parametersが設定されているからです。

これを解消するために、独自のカラムを許可するようにコントローラに設定します。

以下の3つのアクションに対して許可するパラメータを設定することができます。

sign_in …サインイン時のパラメータを設定する。

sign_up …登録時のパラメータを設定する。

account_update …更新時のパラメータを設定する。

今回僕は、更新時のみ独自のパラメータを許可したかったので、次のように書きました。

ちなみに、独自に設定したカラムは、zip、address、introductionの3つです。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:account_update, keys: [:zip, :address, :introduction])
  end
end

まず、configure_permitted_parametersメソッドを定義して、permitの引数にアクション名と許可したいパラメータを渡します。

今回はupdate時の設定をしたいので、第1引数に:account_updateを、第2引数に独自に設定したカラム名を配列で渡します。

そして、before_actionに定義したconfigure_permitted_parametersメソッドを設定します。

後置ifでif: :devise_controller?とありますが、これはdeviseを生成した時にできるヘルパーメソッドで、deviseに関係する画面に遷移した時にという意味があります。

これで、更新時に独自に設定したカラムが許可されるようになりました。

追加したカラムにバリデーションを追加する

独自のカラムが受け付けられるようになったところで、今度はバリデーションも設定しておきます。

例えば上記で追加したzipというカラムは郵便番号なので、整数7桁のみを許容するという動作にしたいです。

これは、deviseで作成したModelにバリデーションの設定を書くことで実現できました。

# app/models/user.rb
class User < ApplicationRecord
  .
    .
  validates :zip, numericality: { only_integer: true }, length: { is: 7 }, allow_nil: true
end

:zipカラム名です。 numericalityonly_integertrueにして、整数のみにマッチするようにしています。 lengthは長さを指定するオプションですが、固定で7文字のみを許容したいので、is: 7を指定しています。

最後に、numericalitynilが許容されないので、allow_nil: truenilを明示的に許容(必須項目でなく)しています。

僕はここでハマってしまったのですが、allow_nilを指定しておかないと、登録時にバリデーションに引っかかってしまい登録ができない状態になってしまいました。

Railsガイドを読んでみたら、

numericalityはデフォルトでnilを許容しない

との記述があり、これが原因だったとわかりました。

つまり、登録時はzipの入力フォームがないためsubmit時にzipはnilとなり、nilを許容しないというバリデーションに引っかかってしまっていた、ということです。

公式に近い情報をよく読むのは大切だなと思いました。

deviese-i18nを使った国際化

deviseで作成されるviewの表示は、デフォルトでは英語です。

英語にも日本語にも対応させたい…という時には、devise-i18nというGemを使うと簡単です。

Gemfileにdevise-i18nを追加して、bundle installを実行します。

# Gemfile
gem 'devise-i18n'

デフォルトの言語設定を日本語に設定します。

# config/application.rb
require_relative "boot"

require "rails/all"

Bundler.require(*Rails.groups)

module BooksApp
  class Application < Rails::Application
    config.load_defaults 6.1
    config.i18n.default_locale = :ja < 追加
  end
end

devise-i18nのviewジェネレーターを実行します。

$ bin/rails g devise:i18n:views
            invoke  Devise::I18n::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  Devise::I18n::MailerViewsGenerator
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb
      invoke  i18n:form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb

なにやらたくさんファイルが生成されましたが、これで日本語化がされているはずです。

サーバーを再起動して確認してみます。

f:id:fuga__ch:20210403163105p:plain

ちゃんと日本語化がされました。

devise-i18nのおかげで、deviseで通常使用されるviewで表示されている文言については、日本語の翻訳ファイルがGemに含まれています。

自動的にそちらを参照してくれるので、日本語の翻訳ファイルをわざわざ作成しなくても翻訳してくれるんです。

特定のviewファイルだけ生成する

最初に紹介したジェネレーターは、deviseのviewファイル全てが含まれています。

ですが、registrationsモジュールに対応するviewだけ作るということもできます。

その場合には、ジェネレーター実行時に-vオプションでviewを生成したモジュールを指定します。

$ bin/rails g devise:i18n:views -v registrations
            invoke  Devise::I18n::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  Devise::I18n::MailerViewsGenerator
      invoke  i18n:form_for
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb

このように、registrationsモジュールに対応するviewのみが作成されます。

僕は今回、registrationsモジュールのedit.html.erbだけが編集できればよかったので、他のファイルは全て削除しました。

それでもちゃんと、他のviewも日本語化がされています。

翻訳ファイルと同じく、viewファイルもGemに含まれているため、viewファイルそのものがなくても自動的にGemの中のviewファイルを参照してくれるので、不必要なファイルをコミットする必要はありません。

✍🏻Tips

このように、最小限のviewファイルだけでも想定通りに翻訳はできます。

が、実際に開発ではviewファイルをデフォルトのままにしておくことはまずないようです。

ただ、devise-i18nの挙動を知るには良い機会でした。

余談(重要)

僕はずっと、「device」だと思っていたのですが、正しくは「devise」でした😅

そして読み方も、「デバイス」ではなく「デバイズ」と濁るようです。

「デバイスって便利だよね〜!」とドヤ顔していると、恥ずかしい思いをするので気をつけましょう🙋🏻‍♂️