Devise を使うことで、簡単にログイン機能を実装できます。 関連してアカウントのロック機能や、メールの送信も実装できます。
よくあるロックされたアカウントの解除には、パスワードの変更がつきものです。 今回は、Devise をベースにロックを解除したら、パスワード変更を強制してみます。
 
目次  
実装 準備 User モデルを作成し、ログインしなくても見ることのできる画面、ログインしないと見ることのできない画面を用意します。 User は、Devise で認証します。
認証前と認証後のページのコントローラとビューを作成 1 2 3 4 5 bundle exec  rails g controller home index --skip-test-framework --skip-assets bundle exec  rails g controller Certified index --skip-test-framework --skip-assets 
 
gem インストール 以下の様に Gemfile に追記する。
Gemfile  
devise の設定変更 devise の設定を変更する。config/initializers/devise.rbについて以下の箇所を設定する。
config/initializers/devise.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 config.scoped_views = true  config.sign_out_all_scopes = false  config.lock_strategy = :failed_attempts  config.unlock_keys = [:email ] config.unlock_strategy = :both  config.maximum_attempts = 10  config.unlock_in = 1 .hour 
 
devise で User モデルを作成 以下のコマンドを実行し devise の model を作成。
1 bundle exec  rails g devise user 
 
今回は、ロック機能を使うので、Lockable のカラムを有効化します。db/migrate/[数字列]_devise_create_users.rbを以下のようにしました。
db/migrate/[数字列]_devise_create_users.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class  DeviseCreateUsers  < ActiveRecord::Migration [5.2 ]  def  change      create_table :users  do  |t |              t.string :email ,              null:  false , default:  ""        t.string :encrypted_password , null:  false , default:  ""               t.string   :reset_password_token        t.datetime :reset_password_sent_at               t.datetime :remember_created_at               t.integer  :failed_attempts , default:  0 , null:  false         t.string   :unlock_token         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 , :unlock_token ,         unique:  true    end  end 
 
作成できたら以下のコマンドでマイグレーション。
1 bundle exec  rails db:migrate 
 
User モデルの修正 app/models/user.rbを以下のようにし、:lockableを付与します。
app/models/user.rb 1 2 3 4 5 6 class  User  < ApplicationRecord         devise :database_authenticatable , :registerable ,          :recoverable , :rememberable , :validatable , :lockable  end 
 
User 用の devise view を作成 次のコマンドでビューを作成。
1 bundle exec  rails g devise:views users  
 
User 用の devise controller を作成 次のコマンドでコントローラーを作成。
1 bundle exec rails generate devise:controllers users 
 
ルーティング修正 config/routes.rbを以下のように修正します。
config/routes.rb 1 2 3 4 5 6 7 Rails .application.routes.draw do   devise_for :users , controllers:  {     sessions:  'users/sessions'    }   get 'certified/index'    root to:  'home#index'  end 
 
テンプレート修正 app/views/layouts/application.html.erbを以下のように修正する。
app/views/layouts/application.html.erb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <!DOCTYPE html > <html >   <head >      <title > Test170DeviseLockPassword</title >      <%=  csrf_meta_tags  %>     <%=  csp_meta_tag  %>     <%=  stylesheet_link_tag    'application' , media:  'all'   %>     <%=  javascript_include_tag 'application'   %>   </head >    <body >      <div >        <p  class ="notice" > <%=  notice  %></p >        <p  class ="alert" > <%=  alert  %></p >      </div >      <div >        <%  if  user_signed_in?  %>         <%=  "LogOnUser: #{current_user.email} "   %>         <%=  link_to 'SignOut' , destroy_user_session_path, method:  :delete   %>       <% else %>         <%=  link_to 'SingUp' , new_user_registration_path  %>         <%=  link_to 'SignIn' , new_user_session_path  %>       <%  end   %>     </div >      <%=  yield   %>   </body >  </html > 
 
Users::SessionsController を修正 ログインできたら、certified/indexにアクセスさせたいです。app/controllers/users/sessions_controller.rbを以下のように変更します。
app/controllers/users/sessions_controller.rb 1 2 3 4 5 class  Users::SessionsController  < Devise::SessionsController   def  after_sign_in_path_for (resource )     certified_index_path   end  end 
 
認証を必須にする app/controllers/certified_controller.rbを修正して、認証されていないと、リダイレクトするようにします。
app/controllers/certified_controller.rb 1 2 3 4 5 6 class  CertifiedController  < ApplicationController   before_action :authenticate_user!    def  index    end  end 
 
メールを設定 開発時は、SMTP サーバーを用意して送信メールを見ることは難しいです。 ブラウザで本来なら送信するメールを参照できるツールletter_opener_webを導入します。
1 2 3 group :development do   gem 'letter_opener_web', '~> 1.0' end 
 
上記を書いたらインストールします。
続いて、config/routes.rbを以下のように修正します。
config/routes.rb 1 2 3 4 5 6 7 8 9 10 Rails .application.routes.draw do   devise_for :users , controllers:  {     sessions:  'users/sessions'    }   get 'certified/index'    root to:  'home#index'       mount LetterOpenerWeb : :Engine , at:  "/letter_opener"  if  Rails .env.development? end 
 
config/environments/development.rbを編集します。2 か所追記します。
config/environments/development.rb 1 2 3 4 5 6 7 Rails .application.configure do         config.action_mailer.default_url_options = { host:  'localhost:3000'  }   config.action_mailer.delivery_method = :letter_opener_web  end 
 
動作確認 bundle exec rails sで起動し動作確認。http://localhost:3000/certified/indexにアクセスするとログイン要求画面に移り、ログインすると/certified/indexに遷移します。
一度ログアウトして、パスワードを 10 回間違えてみます。 アカウントがロックされます。
http://localhost:3000/letter_opener/にアクセスすると、本来なら送信しているメールを見ることができます。 リンクからアカウントのロックを解除できます。
ここまで出来たら、ロック解除の時にパスワード変更を強制する実装に移ります。
ロック解除時パスワード変更を強制する実装 それでは、ロック解除したときに、パスワード変更を強制する実装を行います。 考慮するのは 2 つです。
ロック解除し、パスワードの変更に進む(正常系) 
ロック解除は行われたが、パスワード変更に進まなかった(異常系) =>次回ログイン時パスワード変更を強制することで対応します 
 
ログイン後のパスワード変更を行うので、独自にパスワード変更画面を実装してゆきます。
モデル修正 パスワード変更要求を行うフラグになるカラムを増やします。
bundle exec rails g migration add_req_pass_change_to_usersを実行します。
db/migrate/[数字列]_add_req_pass_ch_to_users.rbを以下のように編集します。
db/migrate/[数字列]_add_req_pass_ch_to_users.rb 1 2 3 4 5 class  AddReqPassChToUsers  < ActiveRecord::Migration [5.2 ]  def  change      add_column :users , :req_pass_change , :boolean , default:  0 , null:  false    end  end 
 
マイグレーションしておきます。
コントローラの編集 ここまでの操作でapp\controllers\users以下には Devise から継承したコントローラーが用意されています。 編集するのは、以下の 2 つです。
app/controllers/users/unlocks_controller.rb 
app/controllers/users/passwords_controller.rb 
 
新規に作成するのは 1 つです。
app/controllers/users/non_token_passwords_controller.rb 
 
また、app/controllers/application_controller.rbも編集します。
ユーザーのパスワードの変更を独自に実装します。
app/controllers/users/unlocks_controller.rbは以下のように編集します。 コメント部分は省略しています。
app/controllers/users/unlocks_controller.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class  Users::UnlocksController  < Devise::UnlocksController      def  create                if  resource_class.find_by(email:  params[:user ][:email ]).locked_at.nil ?       set_flash_message! :notice , :unlocked        redirect_to new_user_session_path       return      end           super    end       def  show                self .resource = resource_class.unlock_access_by_token(params[:unlock_token ])     yield  resource if  block_given?          token = self .set_reset_password_token          self .resource.req_pass_change = true                self .resource.save(validate:  false ) unless  self .resource.new_record?     if  resource.errors.empty?                 set_flash_message! :notice , :unlocked        respond_with_navigational(resource){ redirect_to edit_user_password_path(reset_password_token:  token) }     else                      respond_with_navigational(resource.errors, status:  :unprocessable_entity ){ render :new  }     end    end             def  set_reset_password_token      raw, enc = Devise .token_generator.generate(resource_class, :reset_password_token )     self .resource.reset_password_token   = enc     self .resource.reset_password_sent_at = Time .now.utc     self .resource.save(validate:  false ) unless  self .resource.new_record?     raw   end  end 
 
app/controllers/users/passwords_controller.rbは以下のように編集します。
app/controllers/users/passwords_controller.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class  Users::PasswordsController  < Devise::PasswordsController            def  update           self .resource = resource_class.reset_password_by_token(resource_params)     yield  resource if  block_given?          self .resource.req_pass_change=false ;     self .resource.save(validate:  false ) unless  self .resource.new_record?;     if  resource.errors.empty?       resource.unlock_access! if  unlockable?(resource)       if  Devise .sign_in_after_reset_password         flash_message = resource.active_for_authentication? ? :updated  : :updated_not_active          set_flash_message!(:notice , flash_message)         resource.after_database_authentication         sign_in(resource_name, resource)       else          set_flash_message!(:notice , :updated_not_active )       end        respond_with resource, location:  after_resetting_password_path_for(resource)     else        set_minimum_password_length       respond_with resource     end    end  end 
 
app/controllers/users/non_token_passwords_controller.rbは以下のように編集します。 ログインしたままパスワード編集をするための実装です。
app/controllers/users/non_token_passwords_controller.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class  Users::NonTokenPasswordsController  < ApplicationController      def  edit      @user  = current_user   end       def  update      current_user.password = params[:user ][:password ]     current_user.req_pass_change=false      if  current_user.save                     sign_in(current_user, bypass:  true )       redirect_to root_path, notice:  '更新しました'      else        render :edit_password_non_token      end    end  end 
 
パスワード編集画面への遷移を強制するための仕組みをapp/controllers/application_controller.rbに作ります。before_action :pass_chainged!だけだと、延々とリダイレクトを繰り返してし、ログアウトもできません。 対策として、:unless => :non_check_pass_chainged?を与えて、パスワード編集画面とログアウトではリダイレクトさせません。
app/controllers/application_controller.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class  ApplicationController  < ActionController::Base   before_action :authenticate_user!    before_action :pass_chainged! , :unless  => :non_check_pass_chainged?       def  pass_chainged!      if  current_user&.req_pass_change       redirect_to users_edit_non_token_password_path     end    end       def  non_check_pass_chainged?      request.path_info.match(/^\/users\/edit_non_token_password/ ) | |     request.path_info.match(/^\/users\/non_token_password/ )| |     request.path_info.match(/^\/users\/sign_out/ )   end  end 
 
view の追加 パスワードの編集画面を増やします。 以下を用意します。
app/views/users/non_token_passwordsディレクトリ 
app/views/users/non_token_passwords/edit.html.erb 
 
app/views/users/non_token_passwords/edit.html.erbは、Devise 標準のパスワード編集画面を移植します。 以下のようになります。
app/views/users/non_token_passwords/edit.html.erb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <h2 > Change your password</h2 > <%=  form_for(@user , url:  users_non_token_password_path, html:  { method:  :put  }) do  |f |  %>   <%=  f.hidden_field :reset_password_token   %>   <div  class ="field" >      <%=  f.label :password , "New password"   %><br  />      <%  if  @minimum_password_length   %>       <em > (<%=  @minimum_password_length   %> characters minimum)</em > <br  />      <%  end   %>     <%=  f.password_field :password , autofocus:  true , autocomplete:  "new-password"   %>   </div >    <div  class ="field" >      <%=  f.label :password_confirmation , "Confirm new password"   %><br  />      <%=  f.password_field :password_confirmation , autocomplete:  "new-password"   %>   </div >    <div  class ="actions" >      <%=  f.submit "Change my password"   %>   </div >  <%  end   %> 
 
ルーティング修正 /config/routes.rbは以下のようになります。
/config/routes.rb 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Rails .application.routes.draw do   devise_for :users , controllers:  {     sessions:       'users/sessions' ,     passwords:      'users/passwords' ,     registrations:  'users/registrations' ,     unlocks:  'users/unlocks'    }   namespace :users  do      get :edit_non_token_password , to:  'non_token_passwords#edit'      put :non_token_password , to:  'non_token_passwords#update'    end    get 'certified/index'    root to:  'home#index'    mount LetterOpenerWeb : :Engine , at:  "/letter_opener"  if  Rails .env.development? end 
 
ここまでで実装完了です。
動作確認 bundle exec rails sで起動し動作確認。http://localhost:3000/certified/indexにアクセスするとログイン要求画面に移り、ログインすると/certified/indexに遷移します。
一度ログアウトして、パスワードを 10 回間違えてまた、アカウントロックさせます。
http://localhost:3000/letter_opener/にアクセスして、リンクからアカウントのロックを解除します。
リンクを踏むと、パスワード変更画面に遷移します。
変更が終わるとログインします。
もし、パスワードの変更をせずに再度ロック解除のリンクを踏むとロック解除メールの送信画面に遷移します。 これはメールのリンクについているロック解除のトークンがすでに無効になっているからです。
すでにロック解除が終わっているので、ログイン画面に遷移します。 既存のパスワードでログインすると、パスワード変更画面に強制で遷移します。
ほかの画面に移動しようとしてもパスワード変更が終わるまで、何度でもこの画面が表示されます。
 
devise を使って、ロック解除と強制パスワード変更を連携させることができました。 追加の改修としては、パスワード変更の強制を延期る機能でしょうか? いわゆる「次回行う」ってやつですね。
devise の
ではでは。