Создание уникальных, труднодоступных "купонных" кодов

Приложение My Rails должно создавать электронные купоны для пользователей. Каждый предоставленный купон должен иметь уникальный код купона, который может быть выкуплен в нашей системе.

Например, купон для бесплатного буррито. User A получает купон на бесплатный буррито, а затем User B получает купон на бесплатный буррито. 2 купона должны иметь уникальные коды купонов.

Каков наилучший способ создания такого кода, который легко подделать? Я не хочу, чтобы у пользователей был высокий уровень успеха в наборе случайных чисел и выкуп других купонов людей.

Думаю, думать об этом как подарочная карта с уникальным номером на спине - это то, что я ищу.

Ответы

Ответ 1

Код должен быть неопровержимым, потому что единственная проверка, которую вы можете выполнить перед тем, как дать пользователю свою награду, - проверить, существует ли код, который они ввели, в вашем списке "выпущенных" кодов.

  • Это означает, что количество всех возможных кодов в этом формате намного больше, чем количество кодов, которые вы хотите опубликовать. В зависимости от того, насколько просто просто попробовать коды (повторите попытку script многократно), вам могут потребоваться все возможные коды, чтобы превышать количество выпущенных кодов на миллион или миллиард или более. Это звучит высоко, но возможно в относительно коротких строках.

  • Это также означает, что коды, которые вы используете, должны выбираться как можно скорее во всех возможных кодах. Это необходимо, чтобы пользователи не выяснили, что наиболее допустимые коды начинаются с "AAA", например. Более сложные пользователи могут заметить, что ваши "случайные" коды используют хакерский генератор случайных чисел (Ruby default rand() быстрый и статистически хороший для случайных данных, но он взломан таким образом, поэтому не используйте его).

Отправной точкой для такого безопасного кода будет вывод криптографического PRNG. Ruby имеет библиотеку securerandom, которую вы можете использовать для получения необработанного кода:

require 'securerandom'
SecureRandom.hex
# => "78c231af76a14ef9952406add6da5d42"

Этот код достаточно длинный, чтобы охватить любое реалистичное количество ваучеров (миллионы каждый для всех на планете), без каких-либо значимых шансов повторения или легко угадать. Однако, немного неудобно печатать с физической копии.

Как только вы знаете, как создать случайный, практически неописуемый код, ваша следующая проблема заключается в понимании пользовательского опыта и определении того, насколько вы реально можете поставить под угрозу безопасность во имя удобства использования. Вы должны иметь в виду ценность для конечного пользователя, и поэтому, как трудно кто-то может попытаться получить действительный код. Я не могу ответить на это за вас, но могу высказать некоторые общие соображения об удобстве использования:

  • Избегайте неоднозначных символов. В печати иногда бывает трудно увидеть разницу между 1, I и l, например. Мы часто понимаем, что это должно быть из контекста, но рандомизированная строка символов не имеет этого контекста. Было бы плохой пользовательский опыт, чтобы попробовать несколько вариантов кода, протестировав 0 vs O, 5 vs S и т.д.

  • Используйте строчные или строчные буквы, но не оба. Чувствительность к регистру не будет понята или не будет следовать некоторым процентам ваших пользователей.

  • Принимать вариации при совпадении кодов. Разрешить пробелы и тире. Возможно, пусть даже 0 и O означают одно и то же. Это лучше всего сделать, обработав входной текст, чтобы он был в правильном случае, разделители разделителей строк и т.д.

  • В распечатке отделите код на несколько мелких частей, пользователю будет проще найти свое место в строке и набрать сразу несколько символов.

  • Не делайте код слишком длинным. Я бы предложил 12 символов, в 3 группах по 4.

  • Здесь интересный - вы можете сканировать код для возможных грубых слов или избегать символов, которые их генерируют. Если ваш код содержал только символы K, U, F, C, тогда была бы большая вероятность оскорбить пользователя. Это обычно не вызывает беспокойства, поскольку пользователи не видят большинство защищенных кодов компьютеров, но они будут напечатаны!

Объединив все это, я могу создать полезный код:

# Random, unguessable number as a base20 string
#  .reverse ensures we don't use first character (which may not take all values)
raw_string = SecureRandom.random_number( 2**80 ).to_s( 20 ).reverse
# e.g. "3ecg4f2f3d2ei0236gi"


# Convert Ruby base 20 to better characters for user experience
long_code = raw_string.tr( '0123456789abcdefghij', '234679QWERTYUPADFGHX' )
# e.g. "6AUF7D4D6P4AH246QFH"


# Format the code for printing
short_code = long_code[0..3] + '-' + long_code[4..7] + '-' + long_code[8..11]
# e.g. "6AUF-7D4D-6P4A"

В этом формате есть 20**12 допустимые коды, что означает, что вы можете выдать миллиард своих кодов, и будет один шанс на четыре миллиона, если пользователь просто угадает правильный. В криптографических кругах это было бы очень плохо (этот код небезопасен для быстрой локальной атаки), но для веб-формы, предлагающей бесплатные burritos для зарегистрированных пользователей, и где вы заметите кого-то, пытающегося четыре миллиона раз с script, это нормально.

Ответ 2

Недавно я написал драгоценный камень купонного кода, который делает то же самое. Алгоритм, заимствованный из модуля CPAN модуля Algorithm:: CouponCode.

Код купона должен быть не только уникальным, но и легко читаемым и типом, пока он еще безопасен. Объяснение и решение Нила замечательно. Этот камень обеспечивает удобный способ сделать это и функцию проверки бонуса.

>> require 'coupon_code'
>> code = CouponCode.generate
=> "1K7Q-CTFM-LMTC"
>> CouponCode.validate(code)
=> "1K7Q-CTFM-LMTC"
>> CouponCode.validate('1K7Q-CTFM-LMTO') # Invalid code
=> nil

Ответ 3

Ключом для создания непогасаемых купонных кодов является большое количество возможных кодов, и только небольшая часть из них действительно действительна. Возьмем, например, 8 символов длиной буквенно-числовых строк:

alphanumeric = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ - 63 символа

В этом случае возможны 63^8 = 248155780267521 возможные коды. Это означает, что если вы выдаете миллиард кодов, вероятность угадать код будет 10^9/63^8 = 0.000004... - 4 в миллионе.

Однако это не мешает запустить script, который продолжает пытаться, пока не выяснит действительный код. Чтобы заблокировать такую ​​атаку грубой силы, вам нужно будет подсчитывать попытки на пользователя и запрещать какой-либо предел.

Если вы ищете библиотеку, которая позволяет полностью настраивать выходные коды купонов (длина, кодировка, префикс, суффикс и шаблон), посмотрите voucher-code-generator-js - библиотека, написанная на JavaScript. Пример использования:

voucher_codes.generate({
    length: 8,
    count: 1000,
});

Он будет генерировать 1000 случайных уникальных кодов, каждый длиной 8 символов.

Другой пример:

voucher_codes.generate({
    pattern: "###-###-###",
    count: 1000,
});

Он будет генерировать 1000 случайных уникальных кодов после заданного шаблона.

Исходный код относительно прост. Бьюсь об заклад, вы можете легко переписать его на любой другой язык, если JS не является вашим любимым;)

Если вам требуется комплексное решение для управления кодами ваучеров (в том числе предотвращение грубой силы), вы можете быть заинтересованы в Voucherify.

Ответ 4

Пойдите с чем-то вроде:

class Coupon < ActiveRecord::Base
  before_save generate_token

  validates_uniqueness_of :token

  def generate_token
    self.token = "#{current_user.id}#{SecureRandom.urlsafe_base64(3)}"
  end

end

EDIT: лучший ответ

Ответ 5

Вы можете, например, используйте случайное число и проверьте, не было ли оно ранее, сохраняя все допустимые коды в базе данных.

Ответ 6

Нарисуйте случайные числа с помощью проверенного генератора (http://en.wikipedia.org/wiki/List_of_pseudorandom_number_generators).

Предположим, вы доставляете 333 купона в день, и они действительны в течение 30 дней. Таким образом, вам нужно сохранить 10000 номеров и убедиться, что фальсификатор не может найти его случайно.

Если ваши цифры имеют 10 значащих цифр (~ 32 бит, ~ 8 шестнадцатеричных цифр), вероятность такого события составляет один миллион. Конечно, вы можете использовать больше.

Ответ 7

У меня был аналогичный случай использования, когда мне приходилось генерировать уникальный/неповторяющийся код для каждого объекта, созданного в системе (в этом вопросе это купон). У меня были следующие требования:

  • Я хотел бы, чтобы длина кода была как можно короче.
  • Я понял, что длина кода в конечном итоге будет по крайней мере до тех пор, пока количество цифр, определяющих количество возможных объектов. Напр. если вы создадите 9999 купонов, код по существу должен быть как минимум 4 цифры.
  • Не должно быть последовательным/легко догадаться.

Я исследовал несколько методов для генерации ключей, включая те, которые основаны на временной шкале, и обнаружил, что большинство методов генерируют длинные коды. Поэтому я решил использовать свою собственную логику следующим образом.

  • Я создаю таблицу db, где создаю только одну запись, которая поддерживает подсчет количества объектов, созданных до сих пор в системе.
  • Затем я префикс и суффикс этого числа с одним символом, каждый случайным образом выбранным из [a-zA-Z0-9]. Этот шаг гарантирует, что, несмотря на то, что номера являются последовательными, невозможно угадать код, если не будут угадываться префикс и суффикс. На основе кодировки [a-zA-Z0-9] для кода будет 3782 (62 * 61) возможностей. Вышеупомянутая кодировка работает для меня, но вы можете использовать кодировку по вашему выбору. Некоторые рекомендации содержатся в наилучшем ответе на этот поток.
  • Каждый раз, когда создается новый объект, количество объектов увеличивается на единицу в db.

В этом подходе количество символов кода будет определяться с помощью:

number of characters of ( count of objects in the system so far ) + 2

Итак, когда вы начнете, количество символов будет равно 3, когда вы достигнете 10 объектов, это будет 4, когда вы достигнете 100 объектов, это будет 5, для 1000 это будет 6 и так далее. Таким образом, система будет масштабироваться сама по себе в зависимости от использования.

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