私のRailsアプリは、ユーザー向けの電子クーポンを生成する必要があります。指定された各クーポンには、システムで利用できる一意のクーポンコードが必要です。
たとえば、無料のブリトーのクーポン。 User A
は無料のブリトーのクーポンを受け取り、その後User B
無料のブリトーのクーポンを受け取ります。 2つのクーポンには一意のクーポンコードが必要です。
簡単に偽造されないこのようなコードを生成するための最良の方法は何ですか?ユーザーが乱数を入力したり、他の人のクーポンを利用したりする成功率が高くなることを望んでいません。
裏面にユニークな番号が付いたギフトカードのように考えるのが私が探しているものだと思います。
ユーザーに報酬を与える前に実行できる唯一の検証は、ユーザーが入力したコードが「発行済み」コードのリストに存在するかどうかを確認することであるため、コードは推測できない必要があります。
つまり、その形式で可能なすべてのコードの数は、発行するコードの数よりもはるかに多いということです。単純にコードを試すのがいかに簡単かによって(スクリプトが繰り返し試行することを考えてください)、発行されたコードを100万倍または10億倍以上上回るために、考えられるすべてのコードが必要になる場合があります。これは高く聞こえますが、比較的短い文字列で可能です。
また、使用するコードは、考えられるすべてのコード内で可能な限りランダムに選択する必要があることも意味します。これは、たとえば、ほとんどの有効なコードが「AAA」で始まることをユーザーが理解しないようにするために必要です。より洗練されたユーザーは、「ランダム」コードがハッキング可能な乱数ジェネレーターを使用していることに気付くかもしれません(RubyのデフォルトのRand()
は高速で、ランダムデータには統計的に適していますが、この方法でハッキングできるため、使用しないでください) 。
このような安全なコードの開始点は、暗号化PRNGからの出力です。 Rubyにはsecurerandom
ライブラリがあり、これを使用して次のような生のコードを取得できます。
require 'securerandom'
SecureRandom.hex
# => "78c231af76a14ef9952406add6da5d42"
このコードは、現実的な数のバウチャー(地球上のすべての人にそれぞれ数百万)をカバーするのに十分な長さであり、繰り返しの意味のある機会や推測が容易ではありません。ただし、物理コピーから入力するのは少し厄介です。
ランダムで事実上推測できないコードを生成する方法を知ったら、次の問題は、ユーザーエクスペリエンスを理解し、ユーザビリティの名の下にセキュリティを現実的に危険にさらすことができる量を決定することです。エンドユーザーにとっての価値、つまり誰かが有効なコードを取得しようとするのがどれほど難しいかを覚えておく必要があります。私はあなたのためにそれに答えることはできませんが、ユーザビリティについていくつかの一般的なポイントを作ることができます:
あいまいな文字は避けてください。印刷では、たとえば1
、I
、l
の違いを確認するのが難しい場合があります。コンテキストから何が想定されているかをよく理解していますが、ランダム化された文字列にはこのコンテキストがありません。 0
vs O
、5
vs S
などをテストして、コードのいくつかのバリエーションを試してみるのは、ユーザーエクスペリエンスが悪いでしょう。
小文字または大文字のいずれかを使用しますが、両方は使用しないでください。大文字と小文字の区別が理解されないか、一部のユーザーが従うことはありません。
コードを照合するときにバリエーションを受け入れます。スペースとダッシュを許可します。おそらく、0
とO
が同じことを意味することを許可することさえできます。これは、入力テキストを処理して適切な場合、区切り文字などを削除することによって行うのが最適です。
印刷では、コードをいくつかの小さな部分に分割します。ユーザーが文字列内の場所を見つけて、一度に数文字を入力するのが簡単になります。
コードを長くしすぎないでください。私は4人の3つのグループで12人のキャラクターを提案します。
これは興味深いものです。コードをスキャンして失礼な言葉を探すか、それらを生成する文字を避けたい場合があります。コードに文字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
の有効なコードがあります。つまり、10億の独自のコードを発行でき、ユーザーが正しいコードを推測する可能性は400万分の1になります。暗号化の分野では非常に悪いですが(このコードは高速のローカル攻撃に対して安全ではありません)、登録ユーザーに無料のブリトーを提供するWebフォームの場合、誰かがスクリプトで400万回試行していることに気付く場合は問題ありません。 。
最近私は coupon-code gem と書いたが、これはまったく同じことをする。 Algorithm :: CouponCodeCPANモジュールから借用したアルゴリズム。
クーポンコードは一意であるだけでなく、安全でありながら読みやすく入力しやすいものでなければなりません。ニールの説明と解決策は素晴らしいです。このgemは、それを行うための便利な方法とボーナス検証機能を提供します。
>> require 'coupon_code'
>> code = CouponCode.generate
=> "1K7Q-CTFM-LMTC"
>> CouponCode.validate(code)
=> "1K7Q-CTFM-LMTC"
>> CouponCode.validate('1K7Q-CTFM-LMTO') # Invalid code
=> nil
推測できないクーポンコードを作成するための鍵は、実際に有効なコードのごく一部だけで、可能なコードの大きなスペースです。たとえば、8文字の長さの英数字の文字列を考えてみましょう。
英数字= 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
- 63文字
この場合、63^8 = 248155780267521
の可能なコードがあります。これは、10億個のコードを発行した場合、コードを推測する確率は10^9/63^8 = 0.000004...
- 400万分の4になることを意味します。
ただし、有効なコードが見つかるまで試行を続けるスクリプトの実行を妨げることはありません。このようなブルートフォース攻撃をブロックするには、ユーザーごとの試行回数をカウントし、制限を超えて禁止する必要があります。
出力クーポンコード(長さ、文字セット、プレフィックス、サフィックス、パターン)の完全なカスタマイズを可能にするライブラリを探している場合は、 voucher-code-generator-js -書かれたライブラリをご覧ください。 JavaScriptで。使用例:
voucher_codes.generate({
length: 8,
count: 1000,
});
それぞれ8文字の長さの1000個のランダムな一意のコードを生成します。
もう一つの例:
voucher_codes.generate({
pattern: "###-###-###",
count: 1000,
});
与えられたパターンに従って1000個のランダムな一意のコードを生成します。
ソースコードは比較的単純です。 JSがお気に入りの言語でない場合は、他の言語に簡単に書き直すことができると思います;)
バウチャーコード管理(ブルートフォース攻撃の防止を含む)の包括的なソリューションが必要な場合は、 バウチャー化 に興味があるかもしれません。
次のようなものを使用してください:
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
編集:ここに より良い答え
実績のあるジェネレーターを使用して乱数を描画します( http://en.wikipedia.org/wiki/List_of_pseudorandom_number_generators )。
1日あたり333枚のクーポンを提供し、これらは30日間有効であると想定します。したがって、10000の番号を保存し、偽造者が偶然に番号を見つけられないようにする必要があります。
数値の有効数字が10桁(〜32ビット、〜8 16進数)の場合、そのようなイベントの確率は100万を超える確率です。もちろんもっと使うことができます。
システム上で作成されたすべてのオブジェクトに対して一意の/繰り返されないコードを生成する必要がある同様のユースケースがありました(この質問では、それはクーポンです)。次の要件がありました。
タイムスタンプベースのキーを含むいくつかの方法を調べたところ、ほとんどの方法で長いコードが生成されることがわかりました。そこで、次のように独自のロジックを採用することにしました。
このアプローチでは、コードの文字数は次のように決定されます。
number of characters of ( count of objects in the system so far ) + 2
したがって、最初は文字数が3になり、10個のオブジェクトに達すると4個になり、100個のオブジェクトに達すると5個になり、1000個に達すると6個になります。このように、システムは使用状況に応じて独自に拡張します。
このアプローチは、最初にコードを生成してから、そのコードがデータベースにすでに存在するかどうかを確認する場合よりもうまく機能しました。その場合、まだ生成されていないコードが見つかるまで、コードを生成し続けます。
エポックタイムスタンプを取得し、ベースエンコードします。レコードをどこかに保存している限り、使用時に有効かどうかを比較できます。
手で入力できるようにする必要がある場合は、いつでも最初の8文字程度まで.string()できます。
あなたは例えばすることができます乱数を使用し、すべての有効なコードをデータベースに保存して、以前に生成されていないかどうかを確認します。