Генерирование идентификатора строки в Instagram или Youtube, как и ruby /ActiveRecord
После создания экземпляра данного объекта модели ActiveRecord мне нужно создать короткую (6-8 символов) уникальную строку, которая будет использоваться в качестве идентификатора в URL-адресах, в стиле URL-адресов фотографий в формате Instagram (например, http://instagram.com/p/P541i4ErdL/, который я просто скремблировал, чтобы быть 404) или Youtube видео URL (например, http://www.youtube.com/watch?v=oHg5SJYRHA0).
Какой лучший способ сделать это? Легче всего просто создать случайную строку, пока она не станет уникальной? Есть ли способ хэширования/перетаскивания целочисленного идентификатора таким образом, что пользователи не могут взломать URL-адрес, изменив один символ (например, я сделал это с ссылкой на 404-й Instagram выше) и попал в новую запись?
Ответы
Ответ 1
Вы можете сделать что-то вроде этого:
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
Здесь хороший метод без столкновения, уже реализованный в plpgsql.
Первый шаг: рассмотрите функцию pseudo_encrypt из вики PG.
Эта функция принимает 32-битное целое число в качестве аргумента и возвращает 32-битное целое число, которое выглядит случайным для человеческого глаза, но однозначно соответствует его аргументу (так что шифрование, а не хеширование). Внутри функции вы можете изменить формулу: (((1366.0 * r1 + 150889) % 714025) / 714025.0)
с помощью другой функции , известной только вам, которая дает результат в диапазоне [0..1] (просто настройка констант, вероятно, будет достаточно хорошей, см. ниже мою попытку сделать именно это). Более подробные объяснения см. В статье wikipedia на 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
wWoCi
2hVQz
PyOoR
cjzW8
bIGoqb
A5tDHb
Результаты выглядят случайными и гарантированно являются уникальными во всем пространстве вывода (2 ^ 32 или около 4 миллиардов значений, если вы также используете все входное пространство с отрицательными целыми числами).
Если бы 4 миллиарда значений были недостаточно широкими, вы можете тщательно объединить два 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-разрядный ключ для неопознанных последовательностей.
-
намного быстрее, если это имеет значение.
Ответ 3
Вы можете хешировать идентификатор:
Digest::MD5.hexdigest('1')[0..9]
=> "c4ca4238a0"
Digest::MD5.hexdigest('2')[0..9]
=> "c81e728d9d"
Но кто-то все еще может догадаться, что вы делаете, и повторять так. Вероятно, лучше использовать хэш на контенте