经常有项目需要使用一些邀请码、激活码、兑换码等唯一的编码!通常的做法是随机生成一个编码,然后进行数据库查询匹配,如果已经存在则再随机生成一个,如是循环!但在编码位数有限(比如: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个字符);
2. 构建进制码
对于 MySQL 数据库中 UNSIGNED INT 的自增字段值,其值范围在 1 – 4294967295
当程序从数据库里读取一条记录后,我们将使用四十二进制对自增字段值进行进制转化,其值范围将在 1 – WaB6U3
为什么要使用四十二进制?
看下十进制 4294967295 对应的四十进制/四十二进制/六十四进制值:11bSYMF/WaB6U3/4gfFC3
可以发现当使用四十进制时需要7个字符,而且两个只需要6个字符
四十二进制只是 UNSIGNED INT 的最大值换算所需字符最少的最小进制;
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;
}
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位(自增字段的进制码 [尾部拼接] 验证码)
验证码是写入数据库生成自增字段时就写入的
验证码是一串随机的字符串(不唯一),其字符个数和编码的个数一致,其字符的内容必须是 g-z;
就像十六进制一样,F 字母以上的其他字母是不需要的,而 g-z 是四十二进制不需要的。
4. 验证编码
对于邀请码、激活码、兑换码等之类的编码,总会有人过来对编码进行验证,下面就介绍如何验证
还记得之前介绍优缺点的时候说过:编码可以反算出其对应的自增字段值
当客户输入一个编码时: 先过滤并保存编码中 g-z 的字符(过滤后可以提取到进制码)
然后将进制码转为十进制的数,查询数据库获取编码记录
最后根据编码记录进行构建编码,与用户输入的编码进行覈对。
编码强化
1. 由于使用了有序的数字和字母进行进制转化,所以根据编码后可以很容易猜到自增字段值以及验证码的内容字母
强化:使用无序且跨用数字和字母进行进制转化和反算
// 验证码内容字母:68hIjklMnoPqrstTwxUz
$binary = ‘W0G5S9ACDyEHiJKLmNOd3YR1u7VpQXZab4c2Befgv’;
2. 当编码为8位,进制码6位时
按照 “编码=截取8位(自增字段的进制码 [尾部拼接] 验证码)” 构建的编码被暴力使用400次即可破译
强化:按照 “编码=截取8位(自增字段的进制码 [有序穿插] 验证码)” 构建的编码被暴力使用11200次即可破译
当编码为8位,进制码1位时
按照 “编码=截取8位(自增字段的进制码 [尾部拼接] 验证码)” 构建的编码被暴力使用1280000000次即可破译
强化:按照 “编码=截取8位(自增字段的进制码 [有序穿插] 验证码)” 构建的编码被暴力使用8960000000次即可破译
当编码为8位,进制码5位时
按照 “编码=截取8位(自增字段的进制码 [尾部拼接] 验证码)” 构建的编码被暴力使用8000次即可破译
强化:按照 “编码=截取8位(自增字段的进制码 [有序穿插] 验证码)” 构建的编码被暴力使用228000次即可破译
数据参考
| 自增字段 | 验证码[g-z] | -> | 进制码 | 拼接编码 | 穿插编码 |