在 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)? 考虑函数内部的r1
和l1
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的主要内容,如果未能解决你的问题,请参考以下文章