こんにちは!kossyです!
さて、今回はdevise-securiyのsession_limitableのソースコードを追ってみたので、備忘録としてブログに残してみたいと思います。
前提
devise-securityのsession_limitableモジュールをincludeしたUserモデルが定義されているものとします。
Devise::Models::SessionLimitable
まずはモジュールのコードをさらっと確認してみます。
# 省略 module Devise module Models # SessionLimited ensures, that there is only one session usable per account at once. # If someone logs in, and some other is logging in with the same credentials, # the session from the first one is invalidated and not usable anymore. # The first one is redirected to the sign page with a message, telling that # someone used his credentials to sign in. module SessionLimitable extend ActiveSupport::Concern include Devise::Models::Compatibility # Update the unique_session_id on the model. This will be checked in # the Warden after_set_user hook in {file:devise-security/hooks/session_limitable} # @param unique_session_id [String] # @return [void] # @raise [Devise::Models::Compatibility::NotPersistedError] if record is unsaved def update_unique_session_id!(unique_session_id) raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted? update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" } end end # Should session_limitable be skipped for this instance? # @return [Boolean] # @return [false] by default. This can be overridden by application logic as necessary. def skip_session_limitable? false end end end end
SessionLimited...から始まる部分をGoogle翻訳で訳してみます。
SessionLimited ensures, that there is only one session usable per account at once.
If someone logs in, and some other is logging in with the same credentials,
the session from the first one is invalidated and not usable anymore.
The first one is redirected to the sign page with a message, telling that someone used his credentials to sign in.SessionLimitedは、アカウントごとに一度に使用できるセッションが1つだけであることを保証します。
誰かがログインし、他の誰かが同じ資格情報でログインしている場合、
最初のセッションからのセッションは無効になり、使用できなくなります。
最初のセッションは、誰かが自分の資格情報を使用してサインインしたことを通知するメッセージとともにサインページにリダイレクトされます。
平たく言ってしまうと、多重セッションを禁ずるモジュールですね。
上記の機能はどのようにして実現しているのでしょうか。
update_unique_session_id!
Devise::Models::SessionLimitableモジュールには2つのメソッドが定義されていて、そのうちの一つがupdate_unique_session_id!メソッドです。
コメントアウト部分を訳してみます。
Update the unique_session_id on the model. This will be checked in
the Warden after_set_user hook in {file:devise-security/hooks/session_limitable}モデル内のunique_session_idをupdateします。
これはWardenのafter_set_userフック内でチェックされます。(fileはdevise-security/hooks/session_limitable)
session_limitableを使うときに必要になる、unique_session_idをWardenのafter_set_userフック内でupdateするメソッドのようです。
def update_unique_session_id!(unique_session_id) raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted? update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" } end end
このメソッドをUserモデルに定義し、間にbinding.pryを挟んだ上で、ログインを試みます。
From: /app/app/models/supervisor.rb:94 Supervisor#update_unique_session_id!: 90: def update_unique_session_id!(unique_session_id) 91: binding.pry 92: raise Devise::Models::Compatibility::NotPersistedError, 'cannot update a new record' unless persisted? 93: => 94: update_attribute_without_validatons_or_callbacks(:unique_session_id, unique_session_id).tap do 95: Rails.logger.debug { "[devise-security][session_limitable] unique_session_id=#{unique_session_id}" } 96: end 97: end $ unique_session_id => nil $ persisted? => true
update_attribute_without_validatons_or_callbacksメソッドを読んでみる必要がありそう。
update_attribute_without_validatons_or_callbacks
pry-byebugを導入していると使える、stepメソッドを使って update_attribute_without_validatons_or_callbacks の処理を見てみます。
From: /usr/local/bundle/gems/devise-security-0.16.0/lib/devise-security/models/compatibility/active_record_patch.rb:34 Devise::Models::Compatibility::ActiveRecordPatch#update_attribute_without_validatons_or_callbacks: 33: def update_attribute_without_validatons_or_callbacks(name, value) => 34: update_column(name, value) 35: end $ name => :unique_session_id $ value => "buGqJFwGDV1bmXa39fgY"
ActiveRecordのupdate_columnメソッドを呼び出している処理でした。
update_columnメソッドでattributeの更新を行っているため、updated_at/updated_onの更新は行われません、、、
Warden::Manager.after_set_user
update_unique_session_id!メソッドの呼び出し元はWarden::Managerクラスのafter_set_userフックでした。
# After each sign in, update unique_session_id. This is only triggered when the # user is explicitly set (with set_user) and on authentication. Retrieving the # user from session (:fetch) does not trigger it. Warden::Manager.after_set_user except: :fetch do |record, warden, options| if record.devise_modules.include?(:session_limitable) && warden.authenticated?(options[:scope]) && !record.skip_session_limitable? if !options[:skip_session_limitable] unique_session_id = Devise.friendly_token warden.session(options[:scope])['unique_session_id'] = unique_session_id record.update_unique_session_id!(unique_session_id) else warden.session(options[:scope])['devise.skip_session_limitable'] = true end end end
skip_session_limitableオプションがfalseでなければ、tokenを生成してwardenのsessionにunique_session_idを代入して、
.update_unique_session_id!メソッドを呼び出しています。
コメントアウト部分を訳してみます。
After each sign in, update unique_session_id.
This is only triggered when the user is explicitly set (with set_user) and on authentication.
Retrieving the user from session (:fetch) does not trigger it.サインインするたびに、unique_session_idを更新します。
これは、ユーザーが(set_userを使用して)明示的に設定され、認証された場合にのみトリガーされます。
セッション(:fetch)からユーザーを取得しても、トリガーされません。
呼び出されるタイミングが記載されていました。
skip_session_limitable?
# Should session_limitable be skipped for this instance? # @return [Boolean] # @return [false] by default. This can be overridden by application logic as necessary. def skip_session_limitable? false end
コメントアウト部分を意訳してみます。
Should session_limitable be skipped for this instance?
@return [Boolean]
@return [false] by default. This can be overridden by application logic as necessary.session_limitableをスキップする必要があるかどうかを制御できます。
もしスキップしたい場合は必要に応じてアプリケーションロジックによって上書きできます。
trueを返すようにすれば、unique_session_idカラムの値を更新しなくなるようでした。