在创建给定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链接那样)并最终获得新记录?
答案 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 extension
的pseudo_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"
但有人仍然可以猜测你正在做什么并以这种方式进行迭代。对内容进行哈希处理可能更好