もふもふ技術部

IT技術系mofmofメディア

devise ユーザーのパスワードを自動生成してメールアドレスだけで会員登録できるようにする

deviseのwikiにあるHow To: Automatically generate password for users (simpler registration)をやってみる。

通常、deviseを使った会員登録ではemailとpasswordの入力が必須になりますが、「会員登録時にパスワードを設定しなくてはならない」という仕様はユーザーの負担になることもあります。 例えば「できるだけ会員登録を簡単にしたい」という要件があるケースでは、パスワードの入力を省略させたいこともあると思います。

そのような場合は、一旦システム側で自動でパスワードを設定してしまうのも手でしょう。 今回はdeviseの機能を利用して、会員登録時にパスワードを自動設定する方法を紹介します。

参考: wiki

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

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

導入から基本的な実装を知りたい場合は最小構成導入からログアウトまでを参照。 先に結論を知りたい方はこちら Userモデルの構造はこんな感じ

 create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "name", 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.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

deviseでUserモデルを作成した場合のデフォルト構成にnameカラムを追加しています。

読んでみる

冒頭から翻訳してみる。(意訳有り)

It's now increasingly common for websites to provide "super fast & easy" registration : the user just gives his e-mail. The confirmation e-mail then contains a password generated for the user. If the user is not happy with the password (either wants a super-weak or super-strong one), the ability to change it is given right away when accessing the confirmation link.

最近のWebサイトではとっても早くて簡単な登録ができることがますます一般的になっている。ユーザーは自分のメールアドレスを提供するだけで良い。確認メールには生成されたパスワードが記載され、もしユーザーがそのパスワードに不満があれば(脆弱すぎたり強すぎたりした場合)、すぐに確認リンクを踏んでパスワードの変更ができるようになる。

記事にあったコードは以下です。

generated_password =  Devise .friendly_token.first(8)
user =  User .create!(:email => email、:password => generated_password)
RegistrationMailer .welcome(user、generated_password).deliver

続いて、上記のコードに関してQA形式で補足があるので、それも訳してみましょう。

Question: - ここではdevise RegistrationControllerのcreateアクションをオーバーライドしていますか? - これを実装したコントローラーのソースをみることはできますか? - 新しく作成されたパスワードを記載したwelcomメッセージの詳細を教えてほしい - パスワードを変更したい場合どうすればいいの?

Answer: 私からの回答はそれほどシンプルでも無いし、簡単でもありません。私は devise RegistrationControllerを上書きしていないですし、全く使用していません。ユーザーがアカウントをシステムに直接作成する手段を提供していません。ワークフローを通過し、その最後に上記のコードを使用してアカウントを作成、電子メールを送信しています。上記のコードに加えてこのコードを使用しています。

sign_in(:user, user)

これによってシームレスに新しいユーザーとしてログインすることが可能です。ログイン機能は、しばらくしてから再度ログインしたいときに利用されます。性質上、パスワードや確認手続き(メールの存在確認)は必要ありません。

お、おう、全然参考にならんぞ。。まあ質問に対しての部分的な回答にはなっています。この人のサイトでは一連のワークフローの最後にメールアドレスを渡して、自動で登録処理、ログインが行われる実装がなされているようです。そして性質上何度もログインするようなサービスでは無いっぽい。。

このままだとあまり得るものがなさそうなので上記のコードを使って新規登録にパスワードを生成するようにしていきたいと思います。

View側の変更を行う

今回やりたいことはパスワードを入れずに新規登録したいということなので、既存の新規登録画面にあるpasswordfieldは不要です。まずはいらないものを消してしまいましょう。

app/views/devise/registrations/new.html.slimにある記述から、passwordに関するinputを削除します。

元の内容はこちら。デフォルトの内容にnameを入力するフィールドを足した形です。

h2
  | Sign up
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :name
    br
    = f.text_field :name
  .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 "devise/shared/links"

ここからpassword_fieldとpassword_confirmationを削除してしまいます。 削除した後のapp/views/devise/registrations/new.html.slimは下記のようになりました。

h2
  | Sign up
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = render "devise/shared/error_messages", resource: resource
  .field
    = f.label :name
    br
    = f.text_field :name
  .field
    = f.label :email
    br
    = f.email_field :email, autofocus: true, autocomplete: "email"
  .actions
    = f.submit "Sign up"
= render "devise/shared/links"

実際の画面はこんな感じです。 devise-how-to-001-image01

コントローラー側の変更を行う

今回はRegistrationsControllerに登録処理を渡したいので、createの部分をオーバーライドします。 createメソッドのコメントアウトを外して、How-toで紹介されていた

generated_password = Devise.friendly_token.first(8)
user = User.create!(:email => email, :password => generated_password)
RegistrationMailer.welcome(user, generated_password).deliver

を入れたいと思います。

controllerに記載するとこんな感じ。createの部分を今回のコードに書き換えています。

controllers/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
    generated_password = Devise.friendly_token.first(8)
    user = User.create!(email: params[:user][:email], password: generated_password, name: params[:user][:name])
    RegistrationMailer.welcome(user, generated_password).deliver
  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: [:name])
  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

※このままではまだ動かないので注意してください。 ついでに今回のコードが何をしているのか中身を解読します。

generated_password = Devise.friendly_token.first(8)

一行目はDevise.friendly_tokenです。 ソースはこちら。base64方式でハッシュ値を生成します。

def self.friendly_token(length = 20)
  # To calculate real characters, we must perform this operation.
  # See SecureRandom.urlsafe_base64
  rlength = (length * 3) / 4
  SecureRandom.urlsafe_base64(rlength).tr('lIO0', 'sxyz')
end

今回はfirst(8)なので、生成された20文字のハッシュ値から先頭の8文字を取り出したものを変数に格納します。 今回のHow-toでやりたいことのコアな部分はbase64方式でランダムなハッシュ値を内部で生成し、passwordにそれを適用することでユーザーのパスワード入力の手間を省きたいということですね。 二行目は

user = User.create!(:email => email, :password => generated_password)

です。ここはdevise関係ないですね。ユーザーを作成しています。ただし、今回の構成ではnameというカラムが存在し、パラメーターの渡し方も少し異なるので、以下のように変更します。

user = User.create!(email: params[:user][:email], password: generated_password, name: params[:user][:name])

入力フォームから渡されたnameとemailを反映させたいので、params経由でデータを取得します。passwordは先ほど生成した値が入った変数generated_passwordを指定します。ついでにロケット記法をやめておきましょう。

Welcomeメールを送るようにする

3行目

RegistrationMailer.welcome(user, generated_password).deliver

ですが、これはdeviseの機能でもなんでもなくメール送信です。新規登録時に生成されたパスワードとwelcomeメッセージを送りたいので、メールの実装もついでにやっておきましょう。

まずはローカルでメールを確認できるようにするために、letter_opener(_web) というgemを入れます。

GemFile

gem 'letter_opener_web'

このようにGemfileに記載し、

$ bundle install 

インストールできたら、確認用のルーティングを生成したいのでroutes.rb

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

を記載します。全体像は以下のようになりました。

routes.rb

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    passwords: "users/passwords"
  }
  root to: 'home#index'
  get 'home/index'
  resource :mypages

  # 以下を追記
  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end
end

最後に、debelopment.rbに

config.action_mailer.delivery_method = :letter_opener_web

を追記します。

Rails.application.configure do
  ~中略
  config.assets.quiet = true
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :letter_opener_web  #これを追記
end

これでletter_opener_web

http://localhost:3000/letter_opener

で確認できるようになりました。ローカル環境でメールを確認する一般的な方法です。

続いてメールの実装です。How-toではRegistrationMailerのwelcomeメソッドをdeliverしているので、今回はそれに従います。(別名でも全然大丈夫です。)

$ rails g mailer registration welcome

これでRegistrationMailerというメーラークラスが作成されます。またコマンドラインでwelcomeを渡しているので、welcomeというメソッドを自動で定義してくれています。

class RegistrationMailer < ApplicationMailer
  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.registration_mailer.welcome.subject
  #
  def welcome
    @greeting = "Hi"
    mail to: "to@example.org"
  end
end

こんな感じになってるはずです。 一度How-toにあったメールの実装に戻りましょう。

RegistrationMailer.welcome(user, generated_password).deliver

実装によるとuserインスタンスとgenerated_passwordを引数に渡すような仕様になっているようです。 今回はメールでパスワードを送りたいので、それっぽく書き換えてみましょう。

class RegistrationMailer < ApplicationMailer
  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.registration_mailer.welcome.subject
  #
  def welcome(user, password)
    @user = user
    @password = password
    mail (
      subject: "ようこそ",
      to: user.email
      )
  end
end

引数を二つとるようにし、送信先を登録したemailに変更します。メールタイトルは今回「ようこそ」となるようにしました。

本文のカスタマイズ

先ほど実行した下記のコマンドによって、いくつかファイルが自動生成されます。

コマンド

$ rails g mailer registration welcome

生成されるファイル

 app/views/registration_mailer/welcome.text.slim
 app/views/registration_mailer/welcome.html.slim

この中の記載がメール本文に当たります。

デフォルトはこんな感じ。

app/views/registration_mailer/welcome.html.slim

h1 Registration#welcome
p = @greeting + ", find me in app/views/registration_mailer/welcome.html.slim"

今回は登録したユーザーの名前と、自動生成したパスワードを表記する構成にします。

app/views/registration_mailer/welcome.html.slim

= @user.name
| さん
p あなたのパスワードは
= @password
|です

こんな感じでシンプルに書き換えました。 では実際に会員登録です。

会員登録後の処理

ここまで正常に実装できていれば、新規登録した場合、レコードが保存され、メールが飛んできます。 今回はsample_user1という名前、emailアドレスはsample@test.comで登録しました。

devise-how-to-001-image02

subjectに「ようこそ」という文字が表記され、登録された名前とpasswordが表示されれば成功です。 一点だけ、このままだとメールを送っただけで何も処理を行っていないので、ユーザーはこのままだと置いてけぼりになってしまいます。 How-toにあるように

sign_in(:user, user)

でサインインしてもいいですが、今回は登録したパスワードとメールアドレスが使えるのか試したいので、sign_inはせず、loginページに飛ばしたいと思います。 registrations_controllerに処理後のリダイレクト処理を追加します。 この一行をcreateメソッドに追加します。

  redirect_to new_user_session_path

追加後のregistrations_controllerの中身は下記のようになります。 controllers/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
    generated_password = Devise.friendly_token.first(8)
    user = User.create!(email: params[:user][:email], password: generated_password, name: params[:user][:name])
    RegistrationMailer.welcome(user, generated_password).deliver
    redirect_to new_user_session_path #追加
  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: [:name])
  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

これで会員登録後、ログインページにリダイレクトされます。 届いたパスワードを確認し、ログインできるか試してみましょう。 今回はログインできたことを確かめるためにログインユーザーの名前を表示させる一行を追加してみたいと思います。

h1
  | Home#index
p
  | Find me in app/views/home/index.html.erb
= current_user&.name
= link_to 'ログアウト', destroy_user_session_path, method: :delete

会員登録した後、letter_openerに記載されているパスワードと登録したemailアドレスでログインできるか試してみましょう。 ログインして下記のように表示されれば成功です!

devise-how-to-001-image03

以上、パスワード不要のログインでした。

実際はconfirmableなどを入れると思うので、メールの実装はそちらによることも多いかと思いますが、パスワード不要でログインしたいという要件があれば是非参考にしていただければと思います。