PHP 数字转字符串互换类

再次更新:发现尽然有现成的Base32方案,无奈已经写好了,还是继续分享出来吧。本方案比Base32相比还是有优势的。
1、自带校验功能
2、解码后可以和数据库中的生成项目对比进行二次验证
3、生成的邀请码是随机,相比Base32规律更难找

本类所在的项目,使用 ThinkPHP 搭建,把 IntConvert.php 放到 app\services 目录下,就可以直接使用app\services\IntConvert 自动调用。如果存放目录后,注意修改下 对应的明明空间。

—-分割线—–

最近 的APP上需要增加一个 邀请码功能,通过邀请码注册。因为内部使用,需要防止 外部用户 随便填写一个 邀请码注册成功。因为主要通过图片的方式进行传播,还不恩那个设计的太长。

初期考虑了两种方案: 最笨的方法是一次生成好,用时随机获取一个。另一个是通过Base64对用户ID进行转换下。分析之后,两种方案都不太理想。

一次生成好方案的坏处:1、占用空间 2、有限制,比如生成1万个,超过1万个用户则无法邀请

使用Base64方案的坏处:1、太容易解密 2、长度太长 3、生成的邀请码太有规律 4、大小写问题(MySQL数据库比选择 *_bin 方式建表,会有重复)

没有合适的方案,准备自己造一个轮子。

设计之初考虑了几个方面:
1、为了减轻服务器的负担,能自带校验功能,校验通过后,再去数据库匹配
2、生成时,不能存在 重复的 值,能通过 系统中某项 唯一的key(比如:user_id)去生成,这样就不用考虑取数据库查询是否已经有重复的记录
3、因为最终还要取数据库匹配和方便用户输入,所以 要做到 大小写可以混用。
4、生成的邀请码不能太有规律

实现的过程如下:

之前看过 Base64 的原理,我们可以仿照他,写个 山寨版 的。使用 A-Z0-9 共36个字符,去掉 4个,正好32个。最重要的是这样不存在大小写的问题了。

对数字进行 二进制处理,Base64 是6个字节一段,我的以5个字节一段,如果不是5的倍数,前面自动补充0

对选择的 32个字符,随机打乱,就可以生成 没有太有规律的 邀请码了。

自带校验功能,对输入的数字md5后,随机选取一位当作验证就可以了,验证通过后,再去数据库匹配。范围 0-F,破解率为 1/16。

按上面的方法写好后,发现 邀请码还是太有规律,比如:10000(AA1),100001(AAZ)。想到前面实现的 校验功能,存在16种值,如果生成 16 个 KeyMap,效果会好很多。最终运行效果如下:

当然,不能完全相信 自带校验功能,还需要结合数据库中验证。本方案另外一个好处是本方法 是可逆转的, 用户ID和 邀请码不匹配,也可以判断出是恶意猜出来的邀请莫。

直接上代码,演示地址:http://www.miaoqiyuan.cn/products/php-int-convert/git/app/test/


<?php
namespace app\services;

/**
 * 类名:IntConvert
 * 作者:mqycn
 * 博客:http://www.miaoqiyuan.cn
 * 源码:https://gitee.com/mqycn/IntConvert
 * 说明:数字字符串互换类,常见的应用场景比如邀请码
 */

class IntConvert {

	/**
	 * KeyMap 在初始时,建议重新生成一下
	 */
	static private $keyMap = [
		'LOXB3V64IGUEYCQF8A72WKSHN915RDZM',
		'DZ7O3VMWHB85CELGI6XQYSRA9N4K1FU2',
		'BZNS6WAG83IDK5M2OCUEF4RLQV917HYX',
		'LXOZQKEI7F49US5N1M36ADRV2YW8CGHB',
		'XRNOYZDGIS45UM3LV7E6QW8FC91BH2KA',
		'UQW7V4YSRD1B6KML32ZANF8E9O5GXHCI',
		'MNQ9Y3I65K4VOGE2L7DHRSXAW1UZBC8F',
		'G1QDUKIM4OX67LNRYEB8V3S9ACF5WZ2H',
		'65I13C7XM9ZFKWNR2DEUHQLYB48AGSVO',
		'53ZK4CG86LOMQRAVNEFI1XU7HS2YB9DW',
		'Q4FC3ZV8GYNRH1297DKXUBE5ALIO6WSM',
		'CSI2ON7MBYRVWH63UX8LQ9FG5DEAKZ14',
		'3D1GWSEN7L9IQ8Y5UMVXACORKBHZ42F6',
		'285V46AGWB9LI3CKM1ONURYSF7QHDXZE',
		'OX7NB2SU8IMKQ1Z9YEWVD6H53C4AFLGR',
		'W9RL6NS74HG15ZID2OCAYFBEM83UQXVK',
	];

	/**
	 * 生成随机Key
	 */
	static public function randomKey() {

		header('content-type: text/text; charset=utf-8');
		echo "	#请复制到 IntConvert 头部\n";
		echo "	static private $" . "keyMap = [\n";

		for ($i = 0; $i < 16; $i++) {
			$keys = self::$keyMap[0];
			$keys_new = '';
			$word = '';

			$len = strlen($keys);
			while ($len > 0) {
				$word = substr($keys, rand(0, $len - 1), 1);
				$keys = str_replace($word, '', $keys);
				$keys_new .= $word;
				$len = strlen($keys);
			}
			echo "		'$keys_new',\n";
		}
		echo "	];\n";
		die();
	}

	/**
	 * 将数字编码为字符串
	 */
	static public function toString($num = 0) {

		// 对传入的数字,取hash的第一位
		$hash = self::getHash($num);

		// 根据Hash,选择不同的字典
		$keymap = self::getKeyMap($hash);

		// 数字转二进制
		$map = self::fixBin(decbin($num));

		// 如果不足10位,前面自动补全对应个数的0
		$len = strlen($map);
		if ($len < 10) {
			$map = substr('00000000000000000000', 0, (10 - $len)) . $map;
		}

		// 按5个一组,编码成数字,根据KeyMap加密
		$keys = '';
		$len = strlen($map);
		for ($index = 0; $index < strlen($map); $index += 5) {
			$keys .= substr($keymap, bindec(substr($map, $index, 5)), 1);
		}

		return $hash . $keys;
	}

	/**
	 * 将字符串编码为数字
	 */
	static public function toInt($str = '') {

		//根据生成规则,最小长度为3
		if (strlen($str) < 3) {
			return false;
		}
		$hash = substr($str, 0, 1);
		$keys = substr($str, 1);

		// 根据Hash,选择不同的字典
		$keymap = self::getKeyMap($hash);

		$bin = '';
		// 根据字典,依次 index,并转换为二进制
		for ($i = 0; $i < strlen($keys); $i++) {
			for ($index = 0; $index < strlen($keymap); $index++) {
				if (strtoupper(substr($keys, $i, 1)) === substr($keymap, $index, 1)) {
					$bin .= self::fixBin(decbin($index));
				}
			}
		}

		// 二进制转换为数字
		$num = bindec($bin);

		if (self::getHash($num) === $hash) {
			return $num;
		} else {
			return false;
		}

	}

	/**
	 * 根据Hash取字典
	 */
	static private function getKeyMap($hash = 'A') {
		return self::$keyMap[hexdec($hash)];
	}

	/**
	 * 不足5位的二进制,自动补全二进制位数
	 */
	static private function fixBin($bin = '110') {
		$len = strlen($bin);
		if ($len % 5 != 0) {
			$bin = substr('00000', 0, (5 - $len % 5)) . $bin;
		}
		return $bin;
	}

	/**
	 * 对数字进行Hash
	 */
	static private function getHash($num = 0) {
		return strtoupper(substr(md5(self::getKeyMap(0) . $num), 1, 1));
	}
}

?>

考虑到不同的项目,根据用户ID生成,可能仍然会存在被猜出来的结果,我在上边的类中专门编写了randomKey方法,直接调用即可。

演示地址:http://www.miaoqiyuan.cn/products/php-int-convert/git/app/test/get_keys.php

所有代码已经上传到 gitee.com,如果对您有所帮助,欢迎 给我点亮 star。项目地址:https://gitee.com/mqycn/IntConvert

发表评论

电子邮件地址不会被公开。 必填项已用*标注