在Rails中动态生成唯一的token

21

我想在控制器中为"user_info_token"列的用户生成一个令牌。然而,我想检查当前没有用户拥有该令牌。这段代码是否足够?

  begin
    @new_token = SecureRandom.urlsafe_base64 
    user = User.find_by_user_info_token(@new_token) 
  end while user != nil 

  @seller.user_info_token = @new_token 

还有更简单的方法吗?

6个回答

56
如果你的令牌足够长,并由加密安全的[伪]随机数生成器生成,则无需验证令牌是否唯一。你不需要在循环中生成令牌。
16个原始源字节已经足够提供有效保证。当格式化为URL安全时,结果会更长。
# Base-64 (url-safe) encoded bytes, 22 characters long
SecureRandom.urlsafe_base64(16)

# Base-36 encoded bytes, naturally url-safe, ~25 characters long
SecureRandom.hex(16).to_i(16).to_s(36)

# Base-16 encoded bytes, naturally url-safe, 32 characters long
SecureRandom.hex(16)

这是因为16字节或128位令牌非唯一的概率非常小,几乎为零。大约生成了264 = 18,446,744,073,709,551,616 = 1.845 x 1019个令牌后,只有50%的机会存在任何重复。如果每秒开始生成十亿个令牌,那么需要大约264/(109*3600*24*365.25) = 600 才有50%的机会发生任何重复。
但你并没有每秒生成十亿个令牌。让我们慷慨地假设你每秒生成一个令牌。即使是一个冲突的50%机会,时间跨度也达到了6000亿年。在那之前,地球将被太阳吞噬。

4
+1 这应该是被接受的答案。它正确、优雅且简单——并且有逻辑实用性的支持。 - djoll
说得非常好。我只有一个小小的挑剔。当你说“格式化为URL安全时,结果会更长。”但这并不会降低重复的概率。Base64编码算法是可逆和确定性的,因此Base64编码令牌更长并不意味着它更随机或独特,因为两个输入密钥始终会Base64编码为相同的输出密钥。如果我误解了你是否暗示Base64输出的增加长度意味着它更随机,那我很抱歉。但我只是想指出这一点。 - Joel

35
我找到的最简洁的解决方案是:
@seller.user_info_token = loop do
  token = SecureRandom.urlsafe_base64
  break token unless User.exists?(user_info_token: token)
end

有些干净的东西,但可能存在重复(尽管很少):

@seller.user_info_token = SecureRandom.uuid

随机UUID的重复概率

编辑:当然,在你的:user_info_token中添加唯一索引。这将更快地搜索具有相同令牌的用户,并且如果恰好在精确相同的时刻保存了2个具有相同令牌的用户,则会引发异常!


4

我有很多模型需要应用独特的标记。因此,我在app/models/concerns/tokened.rb中创建了一个Tokened关注点。

module Tokened

  extend ActiveSupport::Concern

  included do
    after_initialize do
      self.token = generate_token if self.token.blank?
    end
  end

  private
    def generate_token
      loop do
        key = SecureRandom.base64(15).tr('+/=lIO0', 'pqrsxyz')
        break key unless self.class.find_by(token: key)
      end
    end
end

任何模型中,我都希望有唯一的标记,我只需执行以下操作:
include Tokened

但是,是的,你的代码看起来也很好。

但是self设置了当前用户的令牌,对吧?因为这里我有@seller和current_user作为两个不同的用户。在这种情况下,我该怎么做呢? - Alain Goldman
我的上面的代码会影响到任何新创建的模型实例(或从数据库加载但尚未设置令牌的模型),其类包括“Tokened”模型。 - deefour

2

这个在Rails4中不使用gem会有问题吗?它在Rails的master分支上 https://github.com/rails/rails/blob/master/activerecord/lib/active_record/secure_token.rb - Batman

1
也许您可以利用实际时间来做一些事情。这样,您就不需要检查令牌是否已被用户使用。
new_token = Digest::MD5.hexdigest(Time.now.to_i.to_s + rand(999999999).to_s)
user.user_info_token = new_token

1
将时间、随机字符串或任何其他“变化”的数据传递到md5中并不能帮助它避免碰撞。 - deefour

0

您可以尝试以下一些技巧来获取唯一的令牌,这是我在我的项目中使用的非常简单的方法 -

CREDIT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

def create_credit_key(count = 25)
    credit_key = ""
    key = CREDIT_CHARS.length
    for i in 1..count
      rand = Random.rand((0.0)..(1.0))
      credit_key += CREDIT_CHARS[(key*rand).to_i].to_s
    end 
    return credit_key
  end

使用摘要算法会更容易,但我在这里尝试生成不使用任何算法的摘要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接