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 の
ではでは。