This page looks best with JavaScript enabled

【Rails】自前で匿名認証を実装する

 ·  ☕ 3 分で読めます
✏️

Railsチュートリアルのremember meの永続化Cookieの実装を参考にする。

「匿名認証」とは

ここでは、

  • ユーザー登録・ログイン不要
  • ブラウザのDBを利用してユーザーを判別できるようにする

という機能を意味する。
「匿名認証」の呼び方はFirebaseのAnonymous Authenticationに倣う。

環境

  • Ruby 2.7.1
  • Rails 6.0.3.2

実装

まず、gem 'bcrypt'を追加する。

Userモデルの作成

rails g model user user_digest:stringでモデルとテーブルを作成する。

1
2
3
4
5
6
7
8
9
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :user_digest, null: false

      t.timestamps
    end
  end
end

user_digestは、ブラウザからログイン中のユーザーを識別するためのカラムになる。

ユーザー作成処理

 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
class User < ApplicationRecord
  attr_accessor :uuid

  class << self
    def create_anonymous!
      uuid = new_uuid
      user = new(
        uuid: uuid,
        user_digest: digest(uuid)
      )
      user.save!
      user
    end

    private

    def new_uuid
      SecureRandom.urlsafe_base64
    end

    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end
  end
end

attr_accessor :uuidは、Userのインスタンスでは保持するが、DBでは保持しない値である。

user = User.new(uuid: 'ゆーゆーあいでぃー')ならuser.uuid => "ゆーゆーあいでぃー"だが、
user = User.firstなどDBから読み込んだ値だとuser.uuid => nilになる。

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
  before_action :set_user

  private

  def set_user
    User.create_anonymous!
  end
end

ApplicationControllerのbefore_actionなので、全アクションの最初にset_userが実行される。

ログイン処理

上記のコードだと、ユーザーがアクセスするたびに新規Userが作成されるだけなので、Userを作成した後にログイン(匿名認証)できるようにする。

  • 初回アクセス時
    • User作成
    • cookieにuser.idとuser.uuidを入れる(uuidが鍵になる)
  • 二度目以降
    • cookieのuser_idとuuidから既存ユーザーを探索してログインする

つまり、初回に作成したランダムなuuidをブラウザ側で保持し、DBに保存したuser_digest(uuidをハッシュ化した値)と突き合わせることでログインする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class User < ApplicationRecord
  attr_accessor :uuid

  # クラスメソッドを省略

  # 下記を追加
  def authenticated?(uuid)
    BCrypt::Password.new(user_digest) == uuid
  end
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module SessionsHelper
  def remember(user)
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:uuid] = user.uuid
  end

  def current_user
    if (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      @current_user ||= user if user && user.authenticated?(cookies[:uuid])
    end
  end
end

current_userは、ログイン状態だと該当のUserインスタンス、ログインしていない状態(初回アクセス時)だとnilを返す。
なので、以下の分岐を入れる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ApplicationController < ActionController::Base
  include SessionsHelper

  before_action :set_user

  private

  def set_user
    # この分岐を追加する
    unless current_user
      user = User.create_anonymous!
      remember(user)
    end
  end
end

補足

current_userのコードについて

Railsチュートリアルに従うと、current_userの中身は以下のようにしている。
しかし、今回は通常ログイン用のsessionが不要なので、cookieだけで判別する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module SessionsHelper
  def log_in(user)
    session[:user_id] = user.id
  end

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:uuid])
        log_in(user)
        @current_user = user
      end
    end
  end
end

Firebaseとの違い

Firebaseの匿名認証では、IndexedDBにユーザーのuuidを格納している。
image

参考

Share on

END
END
@aiandrox

目次