▪ 前言
经常有项目需要使用一些邀请码、激活码、兑换码等唯一的编码!通常的做法是随机生成一个邀请码,然后进行数据库查询匹配,如果已经存在则再随机生成一个,如是循环!但在编码位数有限(比如:8位的邀请码)和已有大量邀请码数据的情况下,上述做法将逐步造成邀请码唯一率生成的概率低下,最终因不停的循环查库导致程序超时或失败!
那么有没有一种邀请码的生成方式不需要进行数据库查询匹配,在其生成的时候就能确定唯一邀请码呢?当然有,而且算法很多,但是像普遍的方法(比如利用时间戳、MD5等)生成的邀请码位数都会比较长,如果你不在意邀请码的长度那么他们将是比较好的方法,但是如果你要求的邀请码是比较短的,那么就需要另寻他法!
▪ 方法
通常我们建立数据库都会有一个自增字段(AUTO_INCREMENT),今天我们就用这个自增字段的值来构建我们所需的邀请码。在你继续研读这篇文章前,你需要先了解其优缺点:
- [优点] 因为使用自增字段的值通过算法构建邀请码,所以写入数据库时不需要进行数据存在检查
- [优点] 通过邀请码可以反算出其对应的自增字段值,那么数据库因为索引的关系速度将非常快
- [缺点] 邀请码含有数字,字母
- [缺点] 邀请码位数必须是 8 位以上
- [缺点] 自增字段的类型需要是 UNSIGNED INT
1. 进制说明
十进制、十六进制对于大家来说比较常见,十进制使用 0-9 来表示,而十六进制则使用 0-9、A-F 来表示
那么使用 0-9、A-Z、a-z 来表示的是什么进制呢?总共有10个数字,26个小写字母、26个大写字母,那就是六十二进制了
同理,四十二进制需要使用 0-9、A-Z、a-f 来表示
使用越大的进制,同一个数值使用的字符数量将越少,比如:16 = F(十进制/十六进制)、 4294967295 = 4gfFC3(十进制/六十二进制)
可以看到 4294967295(10个字符) 数值当使用六十二进制来表示的时候是 4gfFC3(仅需6个字符,少了 4 个字符)
2. 构建进制码
对于 MySQL 数据库中 UNSIGNED INT 的自增字段值,其值范围在 1 - 4294967295
当程序从数据库里读取一条记录后,我们如果使用四十二进制对自增字段值进行进制转化,其值范围将在 1 - WaB6U3
为什么要使用四十二进制?
看下十进制 4294967295 对应的四十进制/四十二进制/六十四进制值:11bSYMF/WaB6U3/4gfFC3
可以发现当使用四十进制时需要7个字符,而另外两个进制只需要6个字符
四十二进制是 4294967295 换算后所需字符最少的最小进制(使用了 0-9、A-Z、a-f 这些字符,剩余的 g-z 预留备用);
// 转为进制数
// 函数自动根据 $birnary 字符数量转为对应的进制值
function toBinary( $num, $binary )
{
$length = strlen($binary);
while( $num > $length - 1 ){ $out = $binary[fmod($num,$length)].$out; $num = floor($num/$length); }
return $binary[$num].$out;
}
// 转为十进制
// 函数自动根据 $birnary 字符数量转为对应的进制值
function toBase( $str, $binary )
{
$size = strlen($str) - 1; $length = strlen($binary);
$str = str_split($str); $out = strpos($binary, array_pop($str));
foreach( $str as $i=>$char ) $out += strpos($binary,$char)*pow($length, $size-$i);
return $out;
}
$binary = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef';
echo toBinary(4294967295,$binary),' = ',toBase('WaB6U3',$binary);
3. 构建邀请码
进制码构建算法确认完后,那么这里我们将引入一个 验证码,数据库中 邀请码表 的字段如下:自增字段、验证码字段;这里我们假设邀请码是8位的,那么:
邀请码 = 截取前8位(基于 自增字段的进制码 [尾部拼接] 验证码)
验证码是写入数据库生成自增字段时就写入的,是一串随机的字符串,其字符个数和邀请码的个数一致,同时其字符的内容必须是 g-z;
就像十六进制一样,F 字母以上的其他字母是十六进制用不到的,而 g-z 是四十二进制用不到的。
4. 验证邀请码
一般邀请码主要用于会员注册用,所以我们的系统需要对邀请码进行验证,下面就介绍如何验证。
还记得之前介绍优缺点的时候说过:邀请码可以反算出其对应的自增字段值
当客户输入一个邀请码时:
- 先过滤并保存邀请码中 g-z 的字符(过滤后可以提取到进制码)
- 然后将进制码转为十进制的数,查询数据库获取邀请码记录
- 最后根据数据库中是否存在该邀请码进行核对。
▪ 强化
1. 无序化
由于使用了有序的数字和字母进行进制转化,所以根据邀请码值可以很容易反推自增字段值以及验证码的内容字母,所以和容易被人破译;所以为了优化这个问题,我们就 使用无序且跨用数字和字母进行进制转化和反算
// 有序的进制字符码
// 验证码内容字母:ghijklmnopqrstuvwxyz
$binary = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef';
// 无序的进制字符码
// 验证码内容字母:358sF7Oh4mEzGqLCeMny
$binary = '0uXa2H9bSdDTfgUNvkloprWtwxAcBIJiKPQ6RjVY1Z';
2. 破译难度分析
当邀请码为8位,其中进制码6位+验证码2位
- 按照 “邀请码 = 截取前8位(基于 自增字段的进制码 [尾部拼接] 验证码)”
基于上面的方式构建的邀请码被暴力使用400次即可破译
- 强化:按照 “邀请码 = 截取前8位(基于 自增字段的进制码 [有序穿插] 验证码)”
基于上面的方式构建的邀请码被暴力使用11200次即可破译
当邀请码为8位,其中进制码1位+验证码7位
- 按照 “邀请码 = 截取前8位(基于 自增字段的进制码 [尾部拼接] 验证码)”
基于上面的方式构建的邀请码被暴力使用1280000000次即可破译
- 强化:按照 “邀请码 = 截取前8位(基于 自增字段的进制码 [有序穿插] 验证码)”
基于上面的方式构建的邀请码被暴力使用8960000000次即可破译
当邀请码为8位,其中进制码5位+验证码3位
- 按照 “邀请码 = 截取前8位(基于 自增字段的进制码 [尾部拼接] 验证码)”
基于上面的方式构建的编码被暴力使用8000次即可破译
- 强化:按照 “邀请码 = 截前取8位(基于 自增字段的进制码 [有序穿插] 验证码)”
基于上面的方式构的邀请码被暴力使用228000次即可破译
数据参考
自增字段值 | 验证码[g-z] | -> | 42进制码 | 邀请码(拼接方式) | 邀请码(穿插方式) |
---|---|---|---|---|---|
1 | ghijklmn | -> | 1 | 1ghijklm | g1hijklm |
130691231 | hijklmno | -> | fffff | fffffhij | hffifffj |
4294967295 | ijklmnop | — | WaB6U3 | WaB6U3ij | WiaB6Uj3 |