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



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

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

--

更新 EOY 2022 --

我已经有一段时间没有回答这个问题了。如此之多,以至于我什至~7年没有看过这个答案。我也看到过许多依赖Rails来运营业务的组织使用过这段代码。

TBH,这些天我不认为我以前的解决方案,或者Rails如何实现它,是一个伟大的解决方案。它使用可以进行PITA调试的回调,并且本质上是悲观的🙁,即使SecureRandom.urlsafe_base64发生冲突的可能性非常低。这适用于长期和短期代币。

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

class ModelName < ActiveRecord::Base
  def persist_with_random_token!(attempts = 10)
    retries ||= 0
    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}")
    token = SecureRandom.urlsafe_base64(16, false)
    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建议,我同意,在此答案中用loop do...break unless...end替换begin...end..while,因为以前的实现将来可能会被删除。

编辑 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

Ryan Bates 在他的 Railscast 中对 beta 邀请使用了一点很好的代码。这将生成一个 40 个字符的字母数字字符串。

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

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

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

本文演示了一些非常巧妙的方法:

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"

如果你想要一些独特的东西,你可以使用这样的东西:

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,只需制作

Base64::decode64(string)

这可能会有所帮助:

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

如果要删除任何特殊字符,请放入第一个参数"+/=",放入第二个参数"0aZ"和15中的任何字符是此处的长度。

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

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

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

试试这个方式:

从 Ruby 1.9 开始,内置了 uuid 生成。使用 SecureRandom.uuid 函数.
在 Ruby 中生成 GUID

这对我很有帮助

你可以用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"

要创建一个正确的 mysql, varchar 32 GUID

SecureRandom.uuid.gsub('-','').upcase
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end

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

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

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

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

相关内容

  • 没有找到相关文章

最新更新