大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Devise】Rememberable

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

deviseのモデル設定に関する記事も連続投稿3投目!

今回はRememberableについてざっくり理解していきたいと思います!

しかし、連続投稿としては今回で最後にします。理由としてはポートフォリオ制作を進めるために他の記事も書きたいためです。なので、別の機会にdeviseについてまた書きたいと思います!(特に、外部認証であるOmniauthableについてはポートフォリオ制作で使っているため、必ず記事にしたいです!)

概要

Rememberableもマイグレーションファイルやモデルにデフォルトで追加されるモデルの設定になります。

[モデル]

devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

[マイグレーションファイル]

## Rememberable
t.datetime :remember_created_at

rememberableで使用するカラムは一つのみです。remember_created_atとあるように

何かの日付を記録するためのカラムを設定しています。何の日付を記憶するのかモジュールを見て推察しましょう。

github.com

今回もまたまた長いモジュールとなっています。

# frozen_string_literal: true

require 'devise/strategies/rememberable'
require 'devise/hooks/rememberable'
require 'devise/hooks/forgetable'

module Devise
  module Models
    # Rememberable manages generating and clearing token for remembering the user
    # from a saved cookie. Rememberable also has utility methods for dealing
    # with serializing the user into the cookie and back from the cookie, trying
    # to lookup the record based on the saved information.
    # You probably wouldn't use rememberable methods directly, they are used
    # mostly internally for handling the remember token.
    #
    # == Options
    #
    # Rememberable adds the following options in devise_for:
    #
    #   * +remember_for+: the time you want the user will be remembered without
    #     asking for credentials. After this time the user will be blocked and
    #     will have to enter their credentials again. This configuration is also
    #     used to calculate the expires time for the cookie created to remember
    #     the user. By default remember_for is 2.weeks.
    #
    #   * +extend_remember_period+: if true, extends the user's remember period
    #     when remembered via cookie. False by default.
    #
    #   * +rememberable_options+: configuration options passed to the created cookie.
    #
    # == Examples
    #
    #   User.find(1).remember_me!  # regenerating the token
    #   User.find(1).forget_me!    # clearing the token
    #
    #   # generating info to put into cookies
    #   User.serialize_into_cookie(user)
    #
    #   # lookup the user based on the incoming cookie information
    #   User.serialize_from_cookie(cookie_string)
    module Rememberable
      extend ActiveSupport::Concern

      attr_accessor :remember_me

      def self.required_fields(klass)
        [:remember_created_at]
      end

      def remember_me!
        self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
        self.remember_created_at ||= Time.now.utc
        save(validate: false) if self.changed?
      end

      # If the record is persisted, remove the remember token (but only if
      # it exists), and save the record without validations.
      def forget_me!
        return unless persisted?
        self.remember_token = nil if respond_to?(:remember_token)
        self.remember_created_at = nil if self.class.expire_all_remember_me_on_sign_out
        save(validate: false)
      end

      def remember_expires_at
        self.class.remember_for.from_now
      end

      def extend_remember_period
        self.class.extend_remember_period
      end

      def rememberable_value
        if respond_to?(:remember_token)
          remember_token
        elsif respond_to?(:authenticatable_salt) && (salt = authenticatable_salt.presence)
          salt
        else
          raise "authenticatable_salt returned nil for the #{self.class.name} model. " \
            "In order to use rememberable, you must ensure a password is always set " \
            "or have a remember_token column in your model or implement your own " \
            "rememberable_value in the model with custom logic."
        end
      end

      def rememberable_options
        self.class.rememberable_options
      end

      # A callback initiated after successfully being remembered. This can be
      # used to insert your own logic that is only run after the user is
      # remembered.
      #
      # Example:
      #
      #   def after_remembered
      #     self.update_attribute(:invite_code, nil)
      #   end
      #
      def after_remembered
      end

      def remember_me?(token, generated_at)
        # TODO: Normalize the JSON type coercion along with the Timeoutable hook
        # in a single place https://github.com/heartcombo/devise/blob/ffe9d6d406e79108cf32a2c6a1d0b3828849c40b/lib/devise/hooks/timeoutable.rb#L14-L18
        if generated_at.is_a?(String)
          generated_at = time_from_json(generated_at)
        end

        # The token is only valid if:
        # 1. we have a date
        # 2. the current time does not pass the expiry period
        # 3. the record has a remember_created_at date
        # 4. the token date is bigger than the remember_created_at
        # 5. the token matches
        generated_at.is_a?(Time) &&
         (self.class.remember_for.ago < generated_at) &&
         (generated_at > (remember_created_at || Time.now).utc) &&
         Devise.secure_compare(rememberable_value, token)
      end

      private

      def time_from_json(value)
        if value =~ /\A\d+\.\d+\Z/
          Time.at(value.to_f)
        else
          Time.parse(value) rescue nil
        end
      end

      module ClassMethods
        # Create the cookie key using the record id and remember_token
        def serialize_into_cookie(record)
          [record.to_key, record.rememberable_value, Time.now.utc.to_f.to_s]
        end

        # Recreate the user based on the stored cookie
        def serialize_from_cookie(*args)
          id, token, generated_at = *args

          record = to_adapter.get(id)
          record if record && record.remember_me?(token, generated_at)
        end

        # Generate a token checking if one does not already exist in the database.
        def remember_token #:nodoc:
          loop do
            token = Devise.friendly_token
            break token unless to_adapter.find_first({ remember_token: token })
          end
        end

        Devise::Models.config(self, :remember_for, :extend_remember_period, :rememberable_options, :expire_all_remember_me_on_sign_out)
      end
    end
  end
end

まず前回までの記事同様に英語の説明部分を見てみます。

Rememberable manages generating and clearing token for remembering the user from a saved cookie. Rememberable also has utility methods for dealing with serializing the user into the cookie and back from the cookie, trying to lookup the record based on the saved information. You probably wouldn't use rememberable methods directly, they are used mostly internally for handling the remember token.

訳: Rememberableは、保存されたcookieからユーザを記憶するためのトークンの生成とクリアを管理する機能です。ユーザーをcookieシリアライズしたり、cookieから戻したり、保存された情報に基づいてレコードを検索したりするためのメソッドもあります。 これらのメソッドは、主に内部で記憶トークンの処理に使用されます。

cookieを使用してユーザーを記憶するためのトークンを生成するとあるようにセッション管理に該当するのかなと思います。つまり、Rememberableはログインしたユーザーの情報を管理している機能ということです。しかし、具体的な機能が少し予想しづらいですね。

モジュールのメソッドを見てその機能を予想していきましょう。

3つのオプション

昨日のRecoverableと同じようにRememberableにも同じように3つのオプションがあるようです。

# Rememberable adds the following options in devise_for:
#
#   * +remember_for+: the time you want the user will be remembered without
#     asking for credentials. After this time the user will be blocked and
#     will have to enter their credentials again. This configuration is also
#     used to calculate the expires time for the cookie created to remember
#     the user. By default remember_for is 2.weeks.
#
#   * +extend_remember_period+: if true, extends the user's remember period
#     when remembered via cookie. False by default.
#
#   * +rememberable_options+: configuration options passed to the created cookie.

英語の説明をざっくり訳すと以下のような感じ

  • remember_for (ユーザーの認証情報の記憶時間の長さ)
  • extend_remember_period (cookieから記憶されているユーザー情報の記憶時間の延長するかどうか)
  • rememberable_options (cookieに渡されるオプションの設定)

ここで2つの疑問があるかと思います。

  1. remember_forextend_remember_periodは依存関係ではないか。
  2. rememberable_optionsでできるオプションの設定とは何か。

まず1についてですが、これは2つの設定は依存関係にあるのかなと思います。remember_forで設定したことを反映させるためにはextend_remember_periodが必要になると思うので、どちらか片方が設定されるということはないと思います。extend_remember_periodtrueに変更することで記憶期間の延長を許可し、remember_forでその記憶期間を設定するという感じですね。

cookieを使用した時のみextend_remember_periodは適応されるということなので、cookieを使わない場合は適応されないのだと思います。ここの違いはあまり理解できませんでした、、

2についてあまり説明している記事が見当たらなかったのですが、下記記事を見つけました。個人の記事なので信頼性はわかりません。

www.bokukoko.info

rememberable_optionsは記事の中で以下のように定義されています。

config.rememberable_options = {:secure => true, :same_site => :none}

cookieの有効を同じサイトにするか、secureなものにするかなどの設定のようです。しかし、これを設定しているGithubの公開リポジトリはあまり見受けられなかったので、そこまで重要な設定ではないのでしょうか。自分はそう感じました。

これもまたdevise.rbで設定するようです。

config.remember_for = 1.week

config.extend_remember_period = false

config.rememberable_options = {}

3つのオプションの設定自体はそこまで重要ではないようですが、ログイン情報を保存する長さの設定など、ログイン情報の記憶に関する機能ということは間違いなさそうです。

モジュールのメソッド

今回もモジュールが長いので使用例の中からメソッドを見ていきましょう。

# == Examples
#
#   User.find(1).remember_me!  # regenerating the token
#   User.find(1).forget_me!    # clearing the token
  • remember_me!

まずはremember_me!メソッドです。

remember_me!

def remember_me!
  self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
  self.remember_created_at ||= Time.now.utc
  save(validate: false) if self.changed?
end

regenerating the tokenとあるように記憶トークンを再度生成しているのだと思われます。

||=を使っているのでremember_tokennilの時self.class.remember_tokenのメソッドで返却される値をremember_tokenに格納しています。

self.classとあるように、モジュールの中にあるモジュールのメソッドを使っているようです。ClassMethodsモジュールがRememberableの中に定義されていました。

module Rememberable
・
・
    module ClassMethods
        # Generate a token checking if one does not already exist in the database.
        def remember_token #:nodoc:
          loop do
            token = Devise.friendly_token
            break token unless to_adapter.find_first({ remember_token: token })
          end
        end
      end
    end

Devise.friendly_tokenトークンを生成しているようです。unless to_adapter.find_first({ remember_token: token })トークンの値が重複しないように制御している感じですね。

そしてremember_created_atカラムを更新しています。

self.remember_created_at ||= Time.now.utc
  • forget_me!

forget_me!メソッドについてはremember_me!の逆の挙動をします。

# If the record is persisted, remove the remember token (but only if
# it exists), and save the record without validations.
def forget_me!
  return unless persisted?
  self.remember_token = nil if respond_to?(:remember_token)
  self.remember_created_at = nil if self.class.expire_all_remember_me_on_sign_out
  save(validate: false)
end

remember_tokenremember_created_atnilを格納していますね。ログイン情報の記憶トークンのリセットをしています。

ある程度これで把握できたかなと思います!