Railsでユニークなトークンを作成する最良の方法は?


156

これが私が使っているものです。トークンは、推測のために必ずしも聞かれる必要はありません。それは何よりも短いURL識別子のようなものであり、私はそれを短く保ちたいです。私はオンラインで見つけたいくつかの例に従いましたが、衝突が発生した場合、以下のコードでトークンが再作成されると思いますが、確信が持てません。しかし、これは縁のあたりが少し荒く感じられるので、より良い提案を見つけたいと思っています。

def self.create_token
    random_number = SecureRandom.hex(3)
    "1X#{random_number}"

    while Tracker.find_by_token("1X#{random_number}") != nil
      random_number = SecureRandom.hex(3)
      "1X#{random_number}"
    end
    "1X#{random_number}"
  end

トークンのデータベース列は一意のインデックスでありvalidates_uniqueness_of :token、モデルでも使用していますが、これらはアプリでのユーザーのアクションに基づいて自動的にバッチで作成されるため(注文してトークンを購入します)、それはアプリにエラーをスローさせることはできません。

また、衝突の可能性を減らすために、最後に別の文字列を追加することもできます。これは、時間に基づいて生成されたものなどですが、トークンが長くなりすぎないようにします。

回答:


333

-更新-

通り1月9日、2015年の溶液を、今で実装されているRailsの5 のActiveRecordのセキュアトークン実装

-Rails 4および3-

将来の参考のために、安全なランダムトークンを作成し、モデルに対して一意であることを確認します(Ruby 1.9とActiveRecordを使用する場合)。

class ModelName < ActiveRecord::Base

  before_create :generate_token

  protected

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless ModelName.exists?(token: random_token)
    end
  end

end

編集:

@kain提案し、私は交換し、合意したbegin...end..whileloop do...break unless...end以前の実装は、将来的に削除される可能性がありますので、この回答で。

編集2:

Rails 4と懸念事項については、これを懸念事項に移すことをお勧めします。

# app/models/model_name.rb
class ModelName < ActiveRecord::Base
  include Tokenable
end

# app/models/concerns/tokenable.rb
module Tokenable
  extend ActiveSupport::Concern

  included do
    before_create :generate_token
  end

  protected

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless self.class.exists?(token: random_token)
    end
  end
end

begin / whileを使用せず、loop / doを使用する
kain

@kain ( "do ... while"タイプのループ)loop doではなく、何らかの理由( "while ... do"タイプのループ)を使用する必要があります(ループは少なくとも1回実行する必要がbegin...whileあります)?
Krule

7
random_tokenはループ内にスコープが設定されているため、この正確なコードは機能しません。
ジョナサン・ムイ

1
@Kruleこれを懸念に変えたのでModelName、メソッド内のも削除すべきではありませんか?代わりにそれを置き換えるかもしれませ self.classんか?そうでなければ、それはあまり再利用できませんね?
パラサイクル

1
ソリューションは廃止予定ではなく、Secure TokenはRails 5に実装されているだけですが、Rails 4またはRails 3(この質問に関連する)では使用できません
Aleks

52

Ryan Batesは、Railscastのベータ招待に素敵なコードを使用しています。これにより、40文字の英数字ストリングが生成されます。

Digest::SHA1.hexdigest([Time.now, rand].join)

3
うん、それは悪くない。私は通常、URLの一部として使用するために、はるかに短い文字列を探しています。
Slick23、2012年

ええ、これは少なくとも読みやすく、理解しやすいです。状況によっては(ベータ招待など)40文字が適切で、これまでのところこれはうまく機能しています。
Nate Bird

12
@ Slick23あなたは、常に、文字列の部分をつかむことができます:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
ビジャン

「クライアントID」をGoogleアナリティクスの測定プロトコルに送信するときに、これを使用してIPアドレスを難読化しています。これはUUIDであると想定されていますが、hexdigest特定のIPの最初の32文字を使用しています。
thekingoftruth 2015年

1
32ビットのIPアドレスの場合、@ thekingoftruthによって生成される可能性のあるすべての16進数のルックアップテーブルを作成するのはかなり簡単です。そのため、ハッシュの部分文字列でさえ元に戻せないとは考えないでください。
mwfearnley 2016年

32

これは応答が遅い場合がありますが、ループの使用を回避するために、メソッドを再帰的に呼び出すこともできます。見た目も手触りも少しすっきりしています。

class ModelName < ActiveRecord::Base

  before_create :generate_token

  protected

  def generate_token
    self.token = SecureRandom.urlsafe_base64
    generate_token if ModelName.exists?(token: self.token)
  end

end

30

この記事では、これを行うためのかなり洗練された方法をいくつか示します。

https://web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/2/creating-small-unique-tokens-in-ruby

リストされている私のお気に入りはこれです:

rand(36**8).to_s(36)
=> "uur0cj2h"

最初の方法は私がやっていることに似ているようですが、randはデータベースにとらわれていませんか?
Slick23、

そして、私がこれに従うかどうかはわかりません:if self.new_record? and self.access_token.nil?...トークンがまだ保存されていないことを確認するために何をチェックしていますか?
Slick23、

4
既存のトークンに対する追加のチェックが常に必要になります。これが明白でないことを知りませんでした。validates_uniqueness_of :tokenマイグレーションでテーブルに一意のインデックスを追加して追加するだけです。
coreyward、

6
ブログ投稿の著者はこちら!はい:この場合は常に、unicityをアサートするためにdb制約などを追加します。
ThibautBarrère、2012

1
(もう存在しない)投稿を探している人のために... web.archive.org/web/20121026000606/http
//blog.logeek.fr/2009/7/…– King'ori Maina

17

ユニークなものが必要な場合は、次のようなものを使用できます。

string = (Digest::MD5.hexdigest "#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}")

ただし、これにより32文字の文字列が生成されます。

ただし、他の方法もあります。

require 'base64'

def after_create
update_attributes!(:token => Base64::encode64(id.to_s))
end

たとえば、IDが10000の場合、生成されるトークンは "MTAwMDA ="のようになります(IDの場合は簡単にデコードできます。

Base64::decode64(string)

一意の文字列を作成するためのメソッドではなく、生成された値が、すでに生成および保存されている値と衝突しないようにすることに、より関心があります。
Slick23、

生成された値は、すでに生成された値と衝突しません-base64は確定的であるため、一意のIDがある場合は、一意のトークンを取得します。
エッセ

私は一緒に行きましたrandom_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]IDは、トークンのIDです。
Slick23、

11
その私には思えるBase64::encode64(id.to_s)のトークンを使用しての目的に反し。ほとんどの場合、トークンを使用してIDを隠し、トークンを持たないユーザーがリソースにアクセスできないようにします。ただし、この場合、誰かが実行するだけBase64::encode64(<insert_id_here>)で、サイト上のすべてのリソースのすべてのトークンを即座に入手できます。
Jon Lemmon 2012

機能するようにこれに変更する必要がありますstring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
カシム

14

これは役に立つかもしれません:

SecureRandom.base64(15).tr('+/=', '0aZ')

最初の引数 '+ / ='に配置するよりも特殊文字を削除したい場合、および2番目の引数 '0aZ'に配置する任意の文字および15はここでの長さです。

そして、余分なスペースと改行文字を削除したい場合は、次のものを追加してください:

SecureRandom.base64(15).tr('+/=', '0aZ').strip.delete("\n")

これが誰にでも役立つことを願っています。


3
「+ / =」のような奇妙な文字が不要な場合は、base64の代わりにSecureRandom.hex(10)を使用できます。
Min Ming Lo

16
SecureRandom.urlsafe_base64同じことを達成します。
反復

7

あなたはhas_secure_token https://github.com/robertomiranda/has_secure_tokenを使用できます

使い方は本当に簡単です

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"

きれいに包まれました!おかげで:D
mswiszcz

1
未定義のローカル変数「has_secure_token」を取得します。何かアイデアはありますか?
Adrian Matteo

3
@AdrianMatteo私はこれと同じ問題を抱えていました。私が理解したことhas_secure_tokenから、Rails 5に付属していますが、私は4.xを使用していました。私はこの記事の手順を実行しましたが、今ではうまくいきます。
Tamara Bernad


5

適切な、mysql、varchar 32 GUIDを作成するには

SecureRandom.uuid.gsub('-','').upcase

単一の文字「-」を置き換えようとしているため、gsubではなくtrを使用できます。 SecureRandom.uuid.tr('-','').upcase。trとgsubの比較については、このリンクを確認してください。
Sree Raj

2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end

0

トークンはパスワードと同じように扱われるべきだと思います。そのため、それらはDBで暗号化する必要があります。

モデルの一意の新しいトークンを生成するために、私はこのようなことをしていません:

key = ActiveSupport::KeyGenerator
                .new(Devise.secret_key)
                .generate_key("put some random or the name of the key")

loop do
  raw = SecureRandom.urlsafe_base64(nil, false)
  enc = OpenSSL::HMAC.hexdigest('SHA256', key, raw)

  break [raw, enc] unless Model.exist?(token: enc)
end
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.