こんにちは!kossyです!
今回は認証機能を提供するGem「devise」のtimeoutableのソースコードを追ってみたので、
備忘録としてブログに残してみたいと思います。
timeoutableとは?
deviseのtimeoutable moduleは、セッションがすでに期限切れになっているかどうかを確認して、
設定された時間が経過した後にセッションが期限切れになると、ユーザーはログインページにリダイレクトされ、もう一度ログインを求められるものです。
導入は簡単で、deviseを用いるモデルに以下を追加するだけで実現できます。
class User < ActiveRecord::Base devise :timeoutable end
また、デフォルトではセッションタイムアウトの時間は30分間ですが、config/initializers/devise.rb内でよしなに変更することができます。
# config/initializers/devise.rb # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. # config.timeout_in = 30.minutes
さて、上記の仕組みはどのようにして実現されているんでしょうか。
ソースコードを追う
メインのソースコードはこちら。
timedout?メソッドが参照されている箇所で判定しているとみて、使用位置を見に行ってみます。
def timedout?(last_access) !timeout_in.nil? && last_access && last_access <= timeout_in.ago end
使用位置はこのファイルでした。
# frozen_string_literal: true # Each time a record is set we check whether its session has already timed out # or not, based on last request time. If so, the record is logged out and # redirected to the sign in page. Also, each time the request comes and the # record is set, we set the last request time inside its scoped session to # verify timeout in the following request. Warden::Manager.after_set_user do |record, warden, options| scope = options[:scope] env = warden.request.env if record && record.respond_to?(:timedout?) && warden.authenticated?(scope) && options[:store] != false && !env['devise.skip_timeoutable'] last_request_at = warden.session(scope)['last_request_at'] if last_request_at.is_a? Integer last_request_at = Time.at(last_request_at).utc elsif last_request_at.is_a? String last_request_at = Time.parse(last_request_at) end proxy = Devise::Hooks::Proxy.new(warden) if !env['devise.skip_timeout'] && record.timedout?(last_request_at) && !proxy.remember_me_is_active?(record) Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope) throw :warden, scope: scope, message: :timeout end unless env['devise.skip_trackable'] warden.session(scope)['last_request_at'] = Time.now.utc.to_i end end end
コメントアウト部分は、
Each time a record is set we check whether its session has already timed outor not, based on last request time.
レコードが設定されるたびに、最後のリクエスト時間に基づいて、そのセッションがすでにタイムアウトしているかどうかを確認します。
If so, the record is logged out and redirected to the sign in page.
Also, each time the request comes and the record is set,
we set the last request time inside its scoped session to verify timeout in the following request.その場合、レコードはログアウトされ、サインインページにリダイレクトされます。
また、リクエストが来てレコードが設定されるたびに、
スコープセッション内の最後のリクエスト時刻を設定して、次のリクエストのタイムアウトを確認します。出典: devise/timeoutable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
実際にtimedout?かどうかの判定を行っているのはこの箇所ですね。
if !env['devise.skip_timeout'] && record.timedout?(last_request_at) && !proxy.remember_me_is_active?(record) Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope) throw :warden, scope: scope, message: :timeout end
Warden内部のコードを読むのは割愛しますが、概ね理解できました。
Wardenの使い方については、下記の記事が詳しかったです。