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


156

这就是我正在使用的。令牌不必一定要猜测,它更像是一个短网址标识符,而不是其他任何东西,我想使其简短。我遵循了一些我在网上找到的示例,如果发生碰撞,我认为下面的代码将重新创建令牌,但是我不确定。不过,我很好奇看到更好的建议,因为这样的感觉有些粗糙。

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模型上使用,但是由于这些是根据用户在应用程序中的操作自动创建的(它们实际上是下订单并购买令牌)而批量创建,因此让应用抛出错误是不可行的。

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

Answers:


333

-更新-

2015年1月9日开始。该解决方案现已在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 loop do在这种情况下(需要循环至少运行一次)应该使用任何原因(“循环...执行”类型的循环)代替begin...while(“循环...执行”类型的循环)?
Krule

7
由于random_token的作用域在循环内,因此该确切的代码将不起作用。
乔纳森·梅

1
@Krule现在您已经将其变成了关注点,您是否还应该摆脱ModelName方法中的?也许self.class改为代替它 ?否则,它不是非常可重用的,是吗?
paracycle

1
该解决方案并未弃用,它是在Rails 5中简单实现的安全令牌,但不能在Rails 4或Rails 3(与该问题有关)中使用
Aleks

52

瑞安·贝茨(Ryan Bates)在Railscast中针对beta版邀请使用了一些代码。这将产生一个40个字符的字母数字字符串。

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

3
是的,那还不错。我通常正在寻找更短的字符串,以用作URL的一部分。
Slick23

是的,这至少很容易阅读和理解。在某些情况下(例如Beta邀请),40个字符非常有用,到目前为止,这对我来说效果很好。
Nate Bird 2012年

12
@ Slick23您还可以随时获取字符串的一部分:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan

在将“客户端ID”发送到Google Analytics(分析)的测量协议时,我用它来混淆IP地址。它应该是UUID,但hexdigest对于任何给定IP ,我只是采用的前32个字符。
thekingoftruth

1
对于32位IP地址,拥有一个由@thekingoftruth生成的所有可能的十六进制摘要的查找表非常容易,因此没有人会想到即使是哈希的子字符串也将是不可逆的。
mwfearnley

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 2011年

6
博客文章的作者在这里!是的:在这种情况下,我总是添加一个db约束或类似约束来声明唯一性。
ThibautBarrère2012年

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,则将具有唯一的令牌。
Esse

我去了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>),他们将立即拥有您站点上每种资源的所有令牌。
乔恩·莱蒙

需要更改为正常工作string = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim

14

这可能会有所帮助:

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

如果要删除除第一个参数'+ / ='之外的任何特殊字符,以及第二个参数'0aZ'和15则是此处的长度。

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

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

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


3
如果您不希望使用诸如“ + / =”之类的怪异字符,则可以只使用SecureRandom.hex(10)而不是base64。
闵縻嗯罗

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。我已按照本文中的步骤进行操作,现在对我有用。
塔玛拉·伯纳德


5

创建适当的mysql varchar 32 GUID

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

由于我们正在尝试替换单个字符“-”,因此可以使用tr而不是gsub。 SecureRandom.uuid.tr('-','').upcase。检查此链接以比较tr和gsub。
Sree Raj

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

0

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

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

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.