我的Rails应用需要为用户生成电子优惠券。每张优惠券都应该有一个独特的优惠券代码,可以在我们的系统上兑换。
例如免费墨西哥卷饼的优惠券。 User A
会收到免费墨西哥卷饼的优惠券,然后User B
会收到免费墨西哥卷饼的优惠券。 2张优惠券应该有独特的优惠券代码。
生成像这样不易伪造的代码的最佳方法是什么?我不希望用户输入随机数和兑换其他人优惠券的成功率很高。
我想这就像背面带有唯一编号的礼品卡一样,是我正在寻找的。 p>
答案 0 :(得分:39)
代码需要是不可取的,因为在向用户提供奖励之前,您可以执行的唯一验证是检查他们输入的代码是否存在于您发布的"已发布的代码中。码。
这意味着该格式中所有可能代码的数量远远大于您要发布的代码数量。根据简单地尝试代码是多么容易(考虑到重复尝试的脚本),那么您可能需要所有可能的代码以超过已发布代码的数量超过一百万或十亿或更多。这听起来很高,但可以在相对较短的字符串中使用。
这也意味着您使用的代码必须在所有可能的代码中随机选择。这是必要的,以避免用户发现大多数有效代码以" AAA"例如。更复杂的用户可能会发现您的"随机"代码使用可破解的随机数生成器(Ruby的默认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
个有效代码,这意味着您可以发出十亿个自己的代码,并且用户只有四分之一的机会猜测一个正确的代码。在加密圈中会非常糟糕(这段代码对快速本地攻击不安全),但是对于向注册用户提供免费卷饼的网络表单,以及你会注意到某人用脚本尝试了四百万次的情况,这是可以的
答案 1 :(得分:8)
最近我写了coupon-code gem,它做了完全相同的事情。该算法借鉴了Algorithm :: CouponCode CPAN模块。
优惠券代码不仅应该是唯一的,还应该易于阅读和打字,同时它仍然是安全的。尼尔的解释和解决方案很棒。这个宝石提供了一种方便的方法和奖励验证功能。
>> require 'coupon_code'
>> code = CouponCode.generate
=> "1K7Q-CTFM-LMTC"
>> CouponCode.validate(code)
=> "1K7Q-CTFM-LMTC"
>> CouponCode.validate('1K7Q-CTFM-LMTO') # Invalid code
=> nil
答案 2 :(得分:4)
创建不可取的优惠券代码的关键是可能代码的大空间,其中只有一小部分实际上是有效的。我们以8个字符长的字母数字字符串为例:
alphanumeric = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
-
63个字符
在这种情况下,有63^8 = 248155780267521
个可能的代码。这意味着如果您发出十亿个代码,则猜测代码的概率将为10^9/63^8 = 0.000004...
- 百万分之四。
然而,它并没有阻止人们运行一个不断尝试的脚本,直到找到有效的代码。为了阻止这种暴力攻击,你需要计算每个用户的尝试次数并禁止某些限制。
如果您正在寻找能够完全自定义输出优惠券代码(长度,字符集,前缀,后缀和模式)的库,请查看voucher-code-generator-js - 用JavaScript编写的库。用法示例:
voucher_codes.generate({
length: 8,
count: 1000,
});
它将生成1000个随机唯一代码,每个代码长8个字符。
另一个例子:
voucher_codes.generate({
pattern: "###-###-###",
count: 1000,
});
它将按照给定的模式生成1000个随机唯一代码。
源代码相对简单。我敢打赌,如果JS不是你最喜欢的那个,你可以轻松地将它重写为任何其他语言;)
如果您需要全面的优惠券代码管理解决方案(包括防止暴力攻击),您可能会对Voucherify感兴趣。
答案 3 :(得分:1)
使用类似的内容:
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
编辑:这是better answer
答案 4 :(得分:0)
你可以,例如通过将所有有效代码存储在数据库中,使用随机数并检查之前是否未生成。
答案 5 :(得分:0)
使用经过验证的生成器(http://en.wikipedia.org/wiki/List_of_pseudorandom_number_generators)绘制随机数。
假设您每天发送333张优惠券,这些优惠券有效期为30天。所以你必须存储10000个数字,并确保伪造者不能偶然找到一个。
如果您的数字有10位有效数字(~32位,~8位十六进制数字),则此类事件发生概率超过一百万。当然你可以使用更多。
答案 6 :(得分:0)
我有一个类似的用例,我必须为系统上创建的每个对象生成一个唯一/非重复的代码(在这个问题中,它是一个优惠券)。我有以下要求:
我探索了几种生成密钥的方法,包括基于时间戳的密钥,并发现大多数方法都生成长代码。所以,我决定采用我自己的逻辑如下。
在这种方法中,代码的字符数将由下式确定:
number of characters of ( count of objects in the system so far ) + 2
因此,当你开始时,你会得到10个字符,当你达到10个对象时它将是4,当你达到100个对象时它将是5,对于1000它将是6,依此类推。这样,系统将根据使用情况自行扩展。
这种方法比首先生成代码然后检查代码是否已经存在于db中的情况更好。在这种情况下,您将继续生成代码,直到找到尚未生成的代码。
答案 7 :(得分:0)
获取一个时期时间戳并对其进行基本编码..只要将它的记录保存在某个地方,就可以比较它在使用时是否有效。
如果您需要手动输入该字符,则始终可以将其.string()减小到前8个左右的字符。