在 ruby​​/ActiveRecord 中生成类似 Instagram 或 Youtube 的不可猜测的字符串 ID

Posted

技术标签:

【中文标题】在 ruby​​/ActiveRecord 中生成类似 Instagram 或 Youtube 的不可猜测的字符串 ID【英文标题】:Generating an Instagram- or Youtube-like unguessable string ID in ruby/ActiveRecord 【发布时间】:2012-09-16 12:03:40 【问题描述】:

在创建给定 ActiveRecord 模型对象的实例时,我需要生成一个简短的(6-8 个字符)唯一字符串以用作 URL 中的标识符,采用 Instagram 的照片 URL 样式(如 http://instagram.com/p/P541i4ErdL/,我只是争先恐后地成为 404)或 Youtube 的视频 URL(如http://www.youtube.com/watch?v=oHg5SJYRHA0)。

执行此操作的最佳方法是什么?重复create a random string 直到它是唯一的,这是最简单的吗?有没有一种方法可以对整数 id 进行散列/洗牌,这样用户就无法通过更改一个字符来破解 URL(就像我对上面的 404'd Instagram 链接所做的那样)并最终获得新记录?

【问题讨论】:

请问您为什么要这样做?随着越来越多的记录被添加,命中模型实例的可能性增加,因为随机字符串非常短,而且很简单,只需要几秒钟即可生成并尝试访问数千个 url。如果你真的想要这种方式的安全性,你将需要一个长字符串。 我想在不暴露其数字 ID 的情况下提供资源的永久链接。如有必要,我可以使用更长的字符串,我只是不想使用巨大的哈希值。 这是一个很好的问题,您可以在 security.stackexchange.com 上提问。这当然不是为了保护资源(正如你用 Instagram 证明的那样)。散列本身并不安全,我不知道您为什么认为它以任何方式使您的网站安全?现实情况是,您正在使用 8 个字符代码保护可公开访问的资源。任何人所要做的就是生成 8 个字符代码并进行 HEAD 连接,直到他们获得 200 个状态 - 你认为这需要多长时间?如果这是您唯一的保护,那几乎是没有任何保护。 这是一个非常好的观点。我将在那里提出一个问题,以了解这一切的原因。 security.stackexchange.com/questions/20718/… 【参考方案1】:

您可以对 id 进行哈希处理:

Digest::MD5.hexdigest('1')[0..9]
=> "c4ca4238a0"
Digest::MD5.hexdigest('2')[0..9]
=> "c81e728d9d"

但有人仍然可以猜到你在做什么并以这种方式进行迭代。对内容进行哈希处理可能会更好

【讨论】:

嗯...这不会导致一堆冲突吗? @kevboh 是的,但您只需要在保存之前检查冲突。您无法避免与短哈希值发生冲突... @Aaron Gilbalter - instagram 的做法(不是 md5)是 62^10。我不认为他们会担心与这样的数字发生冲突 :) 即使在 16^10 时也不太可能。 @pguardiario 那么他们是怎么做到的呢? 做一个简单的散列会破坏这一点。但是,你可以用盐做一个简单的哈希,这在这个用例中非常好: md5(somesalt[0-9]+) - 如果你使用一个像样的盐,那么你很高兴。【参考方案2】:

你可以这样做:

random_attribute.rb

module RandomAttribute

  def generate_unique_random_base64(attribute, n)
    until random_is_unique?(attribute)
      self.send(:"#attribute=", random_base64(n))
    end
  end

  def generate_unique_random_hex(attribute, n)
    until random_is_unique?(attribute)
      self.send(:"#attribute=", SecureRandom.hex(n/2))
    end
  end

  private

  def random_is_unique?(attribute)
    val = self.send(:"#attribute")
    val && !self.class.send(:"find_by_#attribute", val)
  end

  def random_base64(n)
    val = base64_url
    val += base64_url while val.length < n
    val.slice(0..(n-1))
  end

  def base64_url
    SecureRandom.base64(60).downcase.gsub(/\W/, '')
  end
end
Raw

user.rb

class Post < ActiveRecord::Base

  include RandomAttribute
  before_validation :generate_key, on: :create

  private

  def generate_key
    generate_unique_random_hex(:key, 32)
  end
end

【讨论】:

这很酷。我不能删除downcase 以在给定大小的空间中获取更多字符串吗? @kevboh 是的。我的意思是整个事情有点老套,但可以工作......出于某种原因,我不希望密钥区分大小写。 我想我现在先考虑一下,稍后再研究一个更完整、无冲突的选项。谢谢。【参考方案3】:

这是一个没有冲突的好方法,已经在 plpgsql 中实现了。

第一步:考虑 PG wiki 中的 pseudo_encrypt 函数。 这个函数接受一个 32 位整数作为参数,并返回一个 32 位整数,它在人眼看来是随机的,但唯一地对应于它的参数(所以这是加密,而不是散列)。在函数内部,您可以更改公式:(((1366.0 * r1 + 150889) % 714025) / 714025.0) 使用另一个函数只有您知道,它会产生 [0..1] 范围内的结果(只需调整常量可能就足够了,请参阅下面我的尝试)。有关更多理论解释,请参阅Feistel cypher 上的***文章。

第二步:将输出数字编码为您选择的字母表。这是一个以所有字母数字字符为基数的 62 位函数。

CREATE OR REPLACE FUNCTION stringify_bigint(n bigint) RETURNS text
    LANGUAGE plpgsql IMMUTABLE STRICT AS $$
DECLARE
 alphabet text:='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
 base int:=length(alphabet); 
 _n bigint:=abs(n);
 output text:='';
BEGIN
 LOOP
   output := output || substr(alphabet, 1+(_n%base)::int, 1);
   _n := _n / base; 
   EXIT WHEN _n=0;
 END LOOP;
 RETURN output;
END $$

下面是我们得到的与单调序列对应的前 10 个 URL:

select stringify_bigint(pseudo_encrypt(i)) from generate_series(1,10) as i;
stringify_bigint ------------------ tWJbwb eDUHNb 0k3W4b w9dtmc woCi 2hVQz PyOoR cjzW8 比戈克 A5tDHb

结果看起来是随机的,并且保证在整个输出空间中是唯一的(2^32 或大约 40 亿个值,如果您也将整个输入空间与负整数一起使用)。 如果 40 亿个值不够宽,您可以小心地组合两个 32 位结果以达到 64 位,同时不会失去输出的唯一性。棘手的部分是正确处理符号位并避免溢出。

关于修改函数以生成自己独特的结果:让我们将函数体中的常量从 1366.0 更改为 1367.0,然后重试上面的测试。看看结果是如何完全不同的:

NprBxb SY38Ob urrF6b OjKVnc vdS7j uEfEB 3zuaT 0fjsab j7OYrb PYiwJb

更新:对于那些可以编译 C 扩展的人来说,pseudo_encrypt() 的一个很好的替代品是 range_encrypt_element() 来自 permuteseq extension,它具有以下优点:

适用于最多 64 位的任何输出空间,并且不必是 2 的幂。

对不可猜测的序列使用 64 位秘密密钥。

要快得多,如果这很重要的话。

【讨论】:

这太棒了!我现在打算使用红宝石解决方案,但将来我可能会转向这个。谢谢。 @daniel-verite:很好的答案!对我有用,除了一件小事:我需要使用 bigints。我看到你自己编写了 pseudo_encrypt() 函数:你能告诉我如何重写它,以允许 bigint 输入而不是 int (bigint->bigint 而不是 int->bigint)? 考虑函数内部的r1l1 16 位块。您想让它们成为 32 位而不是 16 位,并相应地调整输出 (2x32=>64)。如果这个提示还不够,我建议使用您的代码版本自行创建一个新问题,因为 cmets 部分太小了。 谢谢!我完全不确定:你能检查一下吗? ***.com/questions/12761346/… 这正是我想要的。我需要把它放在列的默认值中。我该怎么做?我应该把select stringify_bigint(pseudo_encrypt(i)) from generate_series(1,1) as i; 作为默认值吗?

以上是关于在 ruby​​/ActiveRecord 中生成类似 Instagram 或 Youtube 的不可猜测的字符串 ID的主要内容,如果未能解决你的问题,请参考以下文章

ruby 在ruby中生成随机字符串

如何在 Ruby 中生成唯一的六位字母数字代码

如何在 Ruby-on-Rails 中生成 PDF 表单

如何在 Ruby 中生成 a 和 b 之间的随机数?

在 Ruby on Rails Web 应用程序中生成图表的首选方式是啥?

ruby Bio Ritmo国际象棋桌挑战:在html文件中生成国际象棋桌。