もふもふ技術部

IT技術系mofmofメディア

メールアドレス以外でサインインできるようにする[devise]

deviseのHow To: Allow users to sign in with something other than their email address, emailアドレス以外でサインインを行う方法について翻訳しつつ実践していきます。

wiki

今回は公式wikiにあるように、usernameというfieldでサインインできるようにしていきますが、適宜置き換えれば好きな名前のカラムをサインイン用のカラムにすることができます。

言語、ライブラリのバージョン

  • Ruby 2.6.3
  • Rails 6.0.2.2
  • devise 4.7.1
  • テンプレートはslimを使用

導入から基本的な実装を知りたい場合は最小構成導入からログアウトまでを参照。

どんなことができるようになるのか

このようにemail以外のユニークなカラムで認証できるようになります。 devise-how-to-use-004-2

事前準備

事前準備といっても、大掛かりにコードを書き換える必要はありません。deviseが提供している設定を変更し、認証キーに設定されているemailカラムを変更しましょう。 デフォルトではconfig/initializers/devise.rbconfig.authentication_keys = [:emil]という設定がコメントアウトされているので、コメントアウトを外して任意のカラムに書き換えます。今回で言えばusernameというカラムにしたいので下記のようにします。

Devise.setup do |config|
 `
 `
 中略 ~
 config.authentication_keys = [:username] #コメントアウトを外して書き換え
 `
 `
end 

これで認証キーに設定されているカラムがusernameに変更されます。

補足: 複数のモデルでdeviseを使用している場合で一部のモデルのキーだけemailにしたくない場合

例えばuserモデル以外に、管理userとしてadmin_userというモデルを利用している状態を想定してみます。どちらもdevise gemを使用して認証を行っているとします。

userモデルはusernameとpasswordという2つのキーで認証し、admin_userはデフォルトのemail, passwordによって認証したいという要件を想定してみましょう。

この要件は、最初に紹介したconfig/initializers/devise.rbにあるキーを書き換える方法は得策ではありません。このファイルの内容はdeviseの全ての設定を担っていますので、個別に変更したい場合はこちらの方法を採用しましょう。 deviseを使用しているmodelに個別に記載を加えます。

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

デフォルトではUserモデルはこのようになっていますので、

  :authentication_keys => [:username]

というオプションを:database_authenticatableに設定しましょう。 追加後のuserモデルはこのようになります。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :authentication_keys => [:username]            :registerable,:recoverable, :rememberable, :validatable
end

:補足終了:

では実装に戻りましょう。config/initializers/devise.rbに追記するか,補足にあるような条件の場合はmodel側に個別に記載が済んだ状態になっていると思います。

以降の作業は共通になります。Rnails側でvalidationを設定しておきたいので、

  validates :email, uniqueness: true
  validates :username, uniqueness: true

もモデルに追加しておきます。email, usernameカラムがユニークであるというバリデーションですね。追記したUserモデルは下記のようになります。emailにもユニーク制約をつけているのはdeviseが提供する他の機能でuserが一意である必要があるためです。

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

  validates :email, uniqueness: true
  validates :username, uniqueness: true
end

ここまでで初期設定の記述は一旦終了です。

ただ、肝心のusernameカラムをまだ用意していないので、migrationfileを作成していきます。

$ rails g migration add_username_to_users username:string:uniq  

この記述でユニークな制約をかけたstring型のusernameカラムをusersテーブルに追加します。 migrationファイルが作成されたらmigrateも続けて行います。

$ rails db:migrate

ここまでで一度schemaファイルを確認しておきましょう。

ActiveRecord::Schema.define(version: 2020_05_13_061723) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "username"
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    t.index ["username"], name: "index_users_on_username", unique: true
  end

end

先ほどのmigrateで上記のようにusernameカラムが作成され、indexが貼られていることを確認してください。

viewファイルをカスタマイズしてusernameを新規登録の際に登録できるようにする

ここまででusernameをモデルのキーにして、userモデルに登録できるようになりましたが、新規登録の画面で登録できるようになっていないため、viewをカスタマイズしていきます。

$ rails g devise:views user

でuserモデルに対応したviewファイルを作成できます 新規登録に関するviewファイルは生成時は下記のようになっているはずです。

app/views/users/registrations/new.html.slim
h2
  | Sign usp
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    br
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :password
    - if @minimum_password_length
      em
        | (
        = @minimum_password_length
        |  characters minimum)
    br
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    br
    = f.password_field :password_confirmation, autocomplete: "new-password"
  .actions
    = f.submit "Sign up"
= render "users/shared/links"

ここにusernameを入力するフォームを入れておきたいので、下記のようにフォームを追加してあげましょう。

h2
  | Sign usp
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :email
    br
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :username
    br
    = f.text_field :username, autocomplete: "username"
  .field
    = f.label :password
    - if @minimum_password_length
      em
        | (
        = @minimum_password_length
        |  characters minimum)
    br
    = f.password_field :password, autocomplete: "new-password"
  .field
    = f.label :password_confirmation
    br
    = f.password_field :password_confirmation, autocomplete: "new-password"
  .actions
    = f.submit "Sign up"
= render "users/shared/links"

このようになりました。

この段階で画面にアクセスしてみると、表示はされるのですが、実際に値を送信して登録することはできません。

値を渡してあげるためにはcontroller側にも少し手を加えてあげる必要があります。操作するのはusers/registrations_controller.rbで、具体的には下記のようになっています。

# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  # before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  # def new
  #   super
  # end

  # POST /resource
  # def create
  #   super
  # end

  # GET /resource/edit
  # def edit
  #   super
  # end

  # PUT /resource
  # def update
  #   super
  # end

  # DELETE /resource
  # def destroy
  #   super
  # end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  # def cancel
  #   super
  # end

  # protected

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_sign_up_params
  #   devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  # end

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_account_update_params
  #   devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  # end

  # The path used after sign up.
  # def after_sign_up_path_for(resource)
  #   super(resource)
  # end

  # The path used after sign up for inactive accounts.
  # def after_inactive_sign_up_path_for(resource)
  #   super(resource)
  # end
end

このファイルの中の

before_action :configure_sign_up_params, only: [:create]

 def configure_sign_up_params
   devise_parameter_sanitizer.permit(:account_update, keys:[:attribute])
 end

コメントアウトを外しておきます。 nameカラムに値を入れたいので、:attributeとなっている部分を:usernameに変えてあげます。これで、usernameを受け取り、登録することができるようになります。

ここまでで一旦動作確認をしておきます。 devise-how-to-004-1

usernameとpasswordでloginできるようにする

新規登録が無事できるようになったので、登録したusernameでログインできるようにしていきましょう。

ログインに対応したviewファイルはapp/views/users/sessions/new.html.slimになります。 内容はデフォルトだと

h2
  | Log in
= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
  .field
    = f.label :email
    br
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .field
    = f.label :password
    br
    = f.password_field :password, autocomplete: "current-password"
  - if devise_mapping.rememberable?
    .field
      = f.check_box :remember_me
      = f.label :remember_me
  .actions
    = f.submit "Log in"
= render "users/shared/links"

このようになっています。 このemailの部分をusernameに変えてあげましょう。

h2
  | Log in
= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
  .field
    = f.label :username
    br
    = f.email_field :username, autocomplete: "username"
  .field
    = f.label :password
    br
    = f.password_field :password, autocomplete: "current-password"
  - if devise_mapping.rememberable?
    .field
      = f.check_box :remember_me
      = f.label :remember_me
  .actions
    = f.submit "Log in"
= render "users/shared/links"

このように、usernameとpasswordで登録するフォームに変更しました。

では実際にアクセスして、試してみます。 devise-how-to-use-004-2

このようにusernameとpasswordでログインできました!

validetableを使用している場合の対処について

これで無事終了と行きたいところなのですが、emailをキーにしていない以上、データの更新や変更にemailを必須とするのはナンセンスなので、modelに幾つかメソッドを追記してバリデーションを無効化させます。

def email_required?
  false
end

def email_changed?
  false
end

この2つのメソッドを追加します。Userモデルにdeviseを入れているので、 追記する場所は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

  validates :email, uniqueness: true
  validates :username, uniqueness: true

  def email_required?
    false
  end

  def email_changed?
    false
  end
end

これでusernameを変更したい場合にemailを必須としないようにできました!

最後に

実際のアプリやサービスではemailを使って認証することが多いと思いますが、簡単なアプリや社内で発行されたIDをキーにして認証を行いたい場合など、emailアドレス以外をキーにしてdeviseの認証を利用することで、認証ロジックをdeviseに任せることができるのはとても良いですね。