在ruby / ActiveRecord中生成类似Instagram或Youtube的不可语句字符串ID

时间:2012-09-25 01:19:57

标签: ruby postgresql activerecord sinatra

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

这样做的最佳方法是什么?最简单的是create a random string反复出现,直到它独一无二?有没有办法散列/随机播放整数id,以便用户不能通过更改一个字符来破解URL(就像我上面的404'd Instagram链接那样)并最终获得新记录?

3 个答案:

答案 0 :(得分:19)

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

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

第二步:以您选择的字母表对输出数进行编码。这是一个在base 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
 wWoCi
 2hVQz
 PyOoR
 cjzW8
 bIGoqb
 A5tDHb

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

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

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

更新:对于那些可以编译C扩展名的人来说,来自permuteseq extensionpseudo_encrypt()的{​​{1}}的良好替代是range_encrypt_element(),具有以下优势:

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

  • 使用秘密的64位密钥进行无法编码的序列。

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

答案 1 :(得分:5)

你可以这样做:

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

答案 2 :(得分:0)

你可以散列id:

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

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