Генерирование идентификатора строки в 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"

Но кто-то все еще может догадаться, что вы делаете, и повторять так. Вероятно, лучше использовать хэш на контенте