【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をサポートします。TwitterやFacebookなどの認証を追加したい場合はこれを使用します。 |
出典:[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
にアクセスしてみます。
すると、登録画面が表示されます。
何も入力せずに「Sign up」をクリックすると…
バリデーションもきっちりされています。
この段階で、「登録」「ログイン」「ログアウト」「パスワードリセット」「ユーザー情報編集」「ユーザー削除」が全て利用できるようになっています。
※パスワードリセットは機能として実装はされていますが、メール送信は別途実装する必要があります。
こんな簡単な手順でこれだけの機能が実装できてしまう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>
これで、全てのページの上部にヘッダーメニューが表示されます。
大切なのは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
配下に生成されます。
deviseでは基本的に、1つのviewファイルを使いまわして複数のModelにも対応しています。
しかし、Modelごとにviewを変えたいという場合もあると思います(僕はまだ出くわしていませんが)。
そういう時用に、Modelごとのviewファイルを作成するジェネレーターも用意されています。
$ rails g devise:views User
User
の部分は、それぞれのModel名に置き換えて使用します。
ジェネレーターを実行すると、Model名に対応したviewファイルが生成されます。
今度は、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
はカラム名です。
numericality
でonly_integer
をtrue
にして、整数のみにマッチするようにしています。
length
は長さを指定するオプションですが、固定で7文字のみを許容したいので、is: 7
を指定しています。
最後に、numericality
はnil
が許容されないので、allow_nil: true
でnil
を明示的に許容(必須項目でなく)しています。
僕はここでハマってしまったのですが、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
なにやらたくさんファイルが生成されましたが、これで日本語化がされているはずです。
サーバーを再起動して確認してみます。
ちゃんと日本語化がされました。
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」でした😅
そして読み方も、「デバイス」ではなく「デバイズ」と濁るようです。
「デバイスって便利だよね〜!」とドヤ顔していると、恥ずかしい思いをするので気をつけましょう🙋🏻♂️