13

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

这样做的最佳方法是什么?重复创建一个随机字符串直到它唯一是最简单的吗?有没有办法以这样一种方式对整数 id 进行散列/混洗,以使用户无法通过更改一个字符来破解 URL(就像我对上面的 404'd Instagram 链接所做的那样)并最终获得新记录?

4

3 回答 3

22

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

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

第二步:在您选择的字母表中编码输出数字。这是一个以所有字母数字字符为基数的 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()from permuteseq extension,它具有以下优点:

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

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

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

于 2012-09-25T19:50:00.800 回答
5

你可以这样做:

随机属性.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

用户.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
于 2012-09-25T16:32:23.343 回答
0

您可以散列 id:

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

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

于 2012-09-25T02:15:21.413 回答