LoginSignup
6
6

More than 3 years have passed since last update.

has_secure_token を後から追加した時に token を埋める

Last updated at Posted at 2020-05-21

結論

既存レコードのバリデーションが通る場合

User.where(token: nil).find_each(&:regenerate_token)

既存レコードのバリデーションが通らない場合

User.where(token: nil).find_each do |user|
  user.token = User.generate_unique_secure_token
  user.save(validate: false)
end

蛇足

背景

Ruby on Rails で動いていたプロジェクトがフロントだけ Next.js 等に移行することになって API サーバーとして動かすことになることってよくあるじゃないですか。で、安直にやるとRails に API トークンを発行させて認証っぽいことを実現したりすることってよくあるじゃないですか。

そんな時に Ruby on Rails 5 から追加された has_secure_token という素晴らしい ActiveRecord のメソッドがあって、データベースに api_token 列を追加しておけば、モデル側はこんな感じで1行追加するだけで勝手に新規作成されたユーザーに api_token を保存してくれます。

class User < ApplicationRecord
  has_secure_token :api_token
end

has_secure_token コードリーディング

has_secure_token の実装は実にシンプルです。本体のコードを読みに行くことに抵抗感のある方は是非読みに行ってみてください。記事執筆時点ではたった数行のシンプルなコードです。
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/secure_token.rb

def has_secure_token(attribute = :token, length: MINIMUM_TOKEN_LENGTH)
  if length < MINIMUM_TOKEN_LENGTH
    raise MinimumLengthError, "Token requires a minimum length of #{MINIMUM_TOKEN_LENGTH} characters."
  end

  # Load securerandom only when has_secure_token is used.
  require "active_support/core_ext/securerandom"
  define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(length: length) }
  before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
end

肝はメソッド最後の2行です。軽く読んでみましょう。has_secure_token には :api_token を与えたものとして読んでいきます。引数を見て分かるようにデフォルトは :token です。

regenerate_api_token メソッドの定義

さて、この define_method では regenerate_api_token のようなメソッドを新たに定義しています。メソッドの実装内容は、.generate_unique_secure_token なる SecureRandom.base58 を返すメソッドでトークンを生成して、update! するというものです。

define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(length: length) }

before_create コールバックで値の代入

最後の行は、 before_create コールバックで先ほどと同様に .generate_unique_secure_token なる SecureRandom.base58 を返すメソッドでトークンを生成して、 api_token に代入してくれています。

before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }

とてもシンプルですね。

本題

さて、ようやくここからが本題ですが、前述の通り has_secure_tokenapi_token にセットしてくれるのは before_create コールバックなので、元々動いていたシステムで後から has_seruce_token をコールしても元々存在していたユーザーの api_token は空のままです。ちょっとしたスクリプトを流して全レコードの api_token を埋めることにしましょう。ここで先ほど define_method されていた regenerate_api_token を使うのが良さそうです。

User.where(api_token: nil).find_each(&:regenerate_api_token)

しかし、ここでお粗末なシステムを運用していたツケが回ってきます。regenerate_api_token は先ほど実装を読んだ通り update! を使って api_token を更新するので、バリデーションが通っていないレコードに対してコールすると例外を投げます。バリデーションが通らないようなレコードがシステムに存在しているのが問題なのは百も承知ですが現実は残酷なもので、今更データを綺麗にするような余裕はありません。バリデーション無効の状態でなんちゃって regenerate_api_token を実現しましょう。ここで、先ほど軽くコードリーディングしてきた経験が活きてきます。regenerate_api_token はトークンの生成に self.class.generate_unique_secure_token というメソッドを使っていることが分かったので、それに倣って一旦代入して save(validate: false) すれば ok です。

User.where(api_token: nil).find_each do |user|
  user.api_token = User.generate_unique_secure_token
  user.save(validate: false)
end

以上。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6