在 Rails 中创建唯一令牌的最佳方法?

问题描述 投票:0回答:12

这是我正在使用的。令牌不一定要听才能猜测,它更像是一个短 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
,但因为这些是根据用户在应用程序中的操作自动批量创建的(他们下订单并购买代币,本质上),让应用程序抛出错误是不可行的。

我想,为了减少冲突的机会,在末尾附加另一个字符串,根据时间生成的东西或类似的东西,但我不希望令牌变得太长。

ruby-on-rails ruby-on-rails-3 random guid
12个回答
349
投票

-- 更新 2022 年 EOY --

我回答这个问题已经有一段时间了。以至于我已经有七年没有看过这个答案了。我还看到许多依赖 Rails 来运行业务的组织使用了这段代码。

说实话,这些天我不会认为我之前的解决方案,或者 Rails 如何实现它,是一个很棒的解决方案。它使用可以通过 PITA 进行调试的回调,并且本质上是悲观的 🙁,尽管

SecureRandom.urlsafe_base64
发生冲突的可能性非常低。这对于长期和短期代币都适用。

我建议的一个可能更好的方法是对此保持乐观😊。在所选数据库中对令牌设置唯一约束,然后尝试保存它。如果保存出现异常,请重试直至成功。

class ModelName < ActiveRecord::Base
  def persist_with_random_token!(attempts = 10)
    retries ||= 0
    self.token = SecureRandom.urlsafe_base64(nil, false)
    save!
  rescue ActiveRecord::RecordNotUnique => e
    raise if (retries += 1) > attempts

    Rails.logger.warn("random token, unlikely collision number #{retries}")
    retry
  end
end

这样做的结果是什么?

  • 减少一个查询,因为我们没有事先检查令牌是否存在。
  • 总体而言,速度要快一些。
  • 不使用回调,这使得调试更容易。
  • 如果发生碰撞,有后备机制。
  • 发生碰撞时的日志跟踪(指标)
    • 也许是时候清理旧令牌了,
    • 或者当我们需要去
      SecureRandom.urlsafe_base64(32, false)
      时我们是否达到了不太可能的记录数量?)。

--更新--

截至 2015 年 1 月 9 日。 该解决方案现已在 Rails 5 ActiveRecord 的安全令牌实现中实现。

-- 导轨 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..while

 替换为 
loop 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
    

51
投票
Ryan Bates 在他的

Railscast on beta 邀请 中使用了一些不错的代码。这会生成一个 40 个字符的字母数字字符串。

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

36
投票
这可能是一个较晚的响应,但为了避免使用循环,您也可以递归地调用该方法。对我来说,它看起来和感觉都稍微干净一些。

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"
    

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

例如,对于 10000 这样的 id,生成的令牌将类似于“MTAwMDA=”(您可以轻松解码它以获取 id,只需 make

Base64::decode64(string)
    

13
投票
这可能会有所帮助:

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

如果您想删除任何特殊字符,请在第一个参数“+/=”中添加任何特殊字符,在第二个参数“0aZ”中添加任何字符,15 是此处的长度。

如果您想删除多余的空格和换行符,请添加以下内容:

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

希望这对任何人都有帮助。


7
投票
试试这个方法:

从 Ruby 1.9 开始,uuid 生成是内置的。使用

SecureRandom.uuid

 功能。

用 Ruby 生成指南

这对我很有帮助


6
投票
您可以使用 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"
    

5
投票
创建正确的 mysql、varchar 32 GUID

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

3
投票
Rails 7,已

内置此功能。请参阅下面的示例:

# Schema: User(token:string, auth_token:string) class User < ActiveRecord::Base has_secure_token has_secure_token :auth_token, length: 36 end user = User.new user.save user.token # => "pX27zsMN2ViQKta1bGfLmVJE" user.auth_token # => "tU9bLuZseefXQ4yQxQo8wjtBvsAfPc78os6R" user.regenerate_token # => true user.regenerate_auth_token # => true
    

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

-1
投票
我认为令牌应该像密码一样处理。因此,它们应该在数据库中加密。

我正在做这样的事情来为模型生成一个独特的新令牌:

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
    
© www.soinside.com 2019 - 2024. All rights reserved.