我遇到的问题
很多时候, 在网页链接中都会显示显示顺序的ID号码. (如: https://abc.com/id=1)
对于面向大众的页面是没有问题的. 有时会遇到下述情况:
- 不想被访客从1顺序遍历所有内容; (被盗取整站所有内容)
- 我的网站比较小, 比希望客户看到个位数的ID; (被客户感觉毫无内容?)
- 签订合同时合同编号写着003; (被客户感觉毫无生意, 一年才做3单?)
- 等等…
我希望的结果
由顺序的ID, 生成无规律的固定长度的字符. 支持正向加密及反向解密. 理论上只能自己解密.
- 1 = 1K2D
- 2 = 113W
- 3 = 114U
- 4 = H159
- 5 = KH6N
- 6 = KK7H
- 7 = 5K8W
- 8 = 159D
- 9 = F1A1
- 10 = 1FBY
我解决的方案
FuzzID v1.1
- 可设置字符串的固定长度. (与显示的最大值相关)
- 可设置安全码. (使各站加密后的值都不同)
- 可设置校验码的位置.
- 可设置返回的字符.
如何使用
- 初始化
$FuzzID = new FuzzID();
- 加密 (id)
$FuzzID->encode($id);
- 解密 (code)
$FuzzID->decode($code);
相应代码
class FuzzID
{
/**--------------------------------------------------------------------------------
* 模糊标识符 FuzzID
* --------------------------------------------------------------------------------
* 功能: 将原十进制的数值, 转化为'三十二进制'无规律的固定长度的字符串.
*
* @version 1.1 @ 2020-02-22
* @author Shadow Chan <shadow@1mmtech.com>
*---------------------------------------------------------------------------------*/
/**
* @var int $Set_length 设置字符串的固定长度
* 具体支持的最大ID值, 请查阅$valid_length中的有效值
* 注意: 设置该数值前, 请务必确认最大ID值能覆盖您的业务范围!!!
*/
private $set_length = 4;
/**
* @var string $set_secures 设置安全码
* 必须满足: 32位长度的16进制字符
* 建议: 使用您熟悉的字符串, 通过MD5计算后的结果 (可方便记忆)
*/
private $set_secures = 'b9aaa8992e7df4ceafc64356964aaa64';
/**
* @var string $set_codes 设置显示码
* 必须满足: 32位长度的不重复字符或数字, 可不按顺序 (如1A2F)
* 默认值: 123456789ABCDEFGHJKMNPQRSTUVWXYZ
* (已省略容易识别出错的: 数字0, 字母O, 字母I, 字母L)
*/
private $set_codes = '123456789ABCDEFGHJKMNPQRSTUVWXYZ';
/**
* @var int $valid_pos 设置校验码所在的位置.
* 从0开始, 需小于$length (不能等于)
* 不需校验码可设为负数
*/
private $set_valid_pos = 3;
//-----------------------------上述为参数设置, 下述将开始相应程序代码-----------------------------//
/**
* @var array $valid_length 校验固定长度支持的最大ID值.
* 固定长度 =>[不带校验码时,带校验码时]
*/
private $valid_length = [
2 => [1024, 32],
3 => [32768, 1024],
4 => [1048576, 32768],
5 => [33554432, 1048576],
6 => [1073741824, 33554432],
7 => [34359738368, 1073741824],
];
/**
* 将原十进制数值加密
* @param int|string $str 需要加密的ID值
* @return false|string 经加密后的字符串 (异常: false=输入的标识符有问题)
*/
public function encode($str)
{
$blocks_num = []; //每个块的二进制代码(仅含数值区)
$blocks_out = []; //每个块的十进制输出码(仅含数值区)
//检查输入问题
if (!is_int($str)) return false; //[错误]输入的并非数字
$max_index = ($this->set_valid_pos < 0) ? 0 : 1; //获取$valid_length相应的位置(是否带校验码)
$max = $this->valid_length[$this->set_length][$max_index]; //获取$valid_length最大支持值
if ($max < (int)$str) return false; //[错误]输入数值大于支持的最大ID值
//将ID转换为二进制数, 并将每5个二进制数作为一个块, 每个块均转为十进制数
$length = ($this->set_valid_pos < 0) ? $this->set_length : $this->set_length - 1;
$str_bin = $this->fillZero(base_convert($str, 10, 2), 5 * $length); //ID转换为二进制数, 并在前面补零.
for ($i = 0; $i < $this->set_length; $i++) { //循环每个块
$block_bin = substr($str_bin, $i * 5, 5); //将每个块的值转换为十进制数.
if ($i !== $this->set_valid_pos) $blocks_num[] = $block_bin; //缓存数值块 (不是验证码块时)
}
//计算最后的数值块
//*根据最后数值块的二进制数转换为十进制数
$last_block_dec = base_convert($blocks_num[count($blocks_num) - 1], 2, 10); //取转换后的十进制数
$blocks_out[] = $last_block_dec; //将转换后的十进制数,存放于$blocks_out
//计算其他的数值块
//*根据该块的二进制数,作为'原始值'
//*根据最后数值块的十进制数,并加上自身所在位置的值,取set_secures该位置字符对应的十进制数,转换为固定四位的二进制数,作为'转换值'
//*向左对齐'原始值'及'转换值'后,进行一一校对;如'转换值'为1时,则'原始值'相应的0转换为1或1转换为0
//*转换后的'原始值'的二进制数,转换为十进制数,存放于$blocks_out
$index_plus = $last_block_dec; //自身所在位置的值
for ($i = count($blocks_num) - 2; $i >= 0; $i--) { //循环剩余每个块
$block_original = $blocks_num[$i]; //获取作为'原始值'
$block_operator = base_convert($this->getSecureByIndex($index_plus), 10, 2);
$block_operator = $this->fillZero($block_operator, 4); //获取'转换值'
for ($j = 0; $j < 4; $j++) { //循环每位二进制数
if ('1' === $block_operator[$j]) { //如'转换值'为1时
$block_original[$j] = ('0' === $block_original[$j]) ? '1' : '0'; //该位的0转换为1或1转换为0.
}
}
$blocks_out[] = base_convert($block_original, 2, 10); //将转换后的十进制数至输出码,存放于$blocks_out
$index_plus++; //增加下个块自身所在位置的值
}
//计算验证区
//*根据验证码的十进制数及所有数值块的十进制数及最后数值块的十进制数的和,除以32求余为0,以得出该验证码
$valid_dec = 32 - ((array_sum($blocks_out) + (int)$last_block_dec) % 32);
//计算输出结果
//*从左至右从大到小排序数值区的数组
//*循环根据每个块的十进制数,将set_codes该位置的字符返回
$blocks_out = array_reverse($blocks_out); //按顺序排列
$final_code = ''; //最终输出码
$num_index = 0; //数值区标记
for ($i = 0; $i < $this->set_length; $i++) {
if ($i === $this->set_valid_pos) {
$final_code .= $this->getCodeByIndex($valid_dec); //验证区的输出
} else $final_code .= $this->getCodeByIndex($blocks_out[$num_index]); //数值区的输出
$num_index++; //增加数值区的标记
}
//返回结果
return $final_code;
}
/**
* 将加密的字符串解密
* @param string $str 需解密的字符串
* @return false|string 经解密的ID值 (异常: false=输入的字符串有问题)
*/
public function decode($str)
{
$blocks_num = []; //每个块的十进制代码(仅含数值区)
$blocks_all = []; //每个块的十进制代码(仅含数值区&验证区)
$blocks_out = []; //每个块的二进制输出码(仅含数值区)
//检查输入问题
if (strlen($str) !== $this->set_length) return false; //[错误]需解密的字符串长度不一致
//将字符转换为十进制数
//*获取每个字符对应$set_codes的位置, 存放于$blocks_num及$blocks_all中
for ($i = 0; $i < $this->set_length; $i++) { //循环每个字符
$dec = $this->getIndexByCode($str[$i]); //获取该字符相应$set_codes的位置
if (false === $dec) return false; //[错误]无效的字符 ($set_codes中不存在)
if ($i !== $this->set_valid_pos) $blocks_num[] = $dec; //设置数值区的代码$blocks_num (不等于验证码位置时)
$blocks_all[] = $dec; //设置所有区的代码$blocks_all
}
//计算最后的数值块
$last_block_dec = $blocks_num[count($blocks_num) - 1]; //取最后块的十进制数
$last_block_bin = base_convert($last_block_dec, 10, 2); //取转换后的二进制数
$blocks_out[] = $this->fillZero($last_block_bin, 5); //将最后块固定五位二进制数,存放于$blocks_out
//判断校验区
//*根据验证码的十进制数及所有数值块的十进制数及最后数值块的十进制数的和,除以32求余为0.
$result = (array_sum($blocks_all) + (int)$last_block_dec) % 32;
if (0 !== $result) return false; //[错误]校验码出错
//计算其他的数值块
//*根据该块的十进制数转化为二进制数并补零至五位,作为'结果值'
//*根据最后块数值的十进制数,并加上自身所在位置的值,取set_secures该位置字符对应的十进制数,转换为固定四位的二进制数,作为'转换值'
//*向左对齐'原始值'及'转换值'后,进行一一校对;如'转换值'为1时,则'原始值'相应的0转换为1或1转换为0
//*转换后的'原始值'的二进制数,存放于$blocks_out
$index_plus = $last_block_dec; //自身所在位置的值
for ($i = count($blocks_num) - 2; $i >= 0; $i--) { //循环剩余每个块
$block_result = base_convert($blocks_num[$i], 10, 2);
$block_result = $this->fillZero($block_result, 5); //获取'结果值'
$block_operator = base_convert($this->getSecureByIndex($index_plus), 10, 2);
$block_operator = $this->fillZero($block_operator, 4); //获取'转换值'
for ($j = 0; $j < 4; $j++) { //循环每位二进制数
if ('1' === $block_operator[$j]) { //如'转换值'为1时
$block_result[$j] = ('0' === $block_result[$j]) ? '1' : '0'; //该位的0转换为1或1转换为0.
}
}
$blocks_out[] = $block_result; //将转换后的二进制数,存放于$blocks_out
$index_plus++; //增加下个块自身所在位置的值
}
//获取ID值
$blocks_out = array_reverse($blocks_out); //按顺序排列
$out_bins = ''; //最终输出的ID值二进制数
foreach ($blocks_out as $dec) $out_bins .= $dec; //循环获取每块的二进制数
return base_convert($out_bins, 2, 10); //返回相应十进制数ID值
}
/**
* 字符或数字前补零
* @param string|int $str 需要补零的字符或数字
* @param int $length 返回数字的长度
* @return false|string 返回补零后的数值 (异常: false=输入的字符串长度超出设置的长度)
*/
private function fillZero($str, int $length)
{
//将$str转为字符串
$str = '' . $str;
//获取字符串的长度
$strlen = strlen($str);
//异常判断
if ($strlen > $length) return false; //超出设定的长度
//循环每位数前补零
for ($i = $strlen; $i < $length; $i++) $str = '0' . $str;
//返回结果
return $str;
}
/**
* 获取当前输入位置相应显示码的字符
* @param int $index 指定$set_codes的位置
* @return string $set_codes指定位置的字符
*/
private function getCodeByIndex($index)
{
//返回结果
return $this->set_codes[$index % 32];
}
/**
* 获取当前输入位置相应安全码的字符
* @param int $index 指定$set_secures位置
* @return string $set_secures指定位置的字符
*/
private function getSecureByIndex($index)
{
//返回结果
return $this->set_secures[$index % 32];
}
/**
* 获取当前输入的字符于显示码的位置
* @param string $char 输入的字符
* @return false|int $set_codes所在的位置 (异常: false=输入的字符不存在于显示码内)
*/
private function getIndexByCode($char)
{
//返回结果
return strpos($this->set_codes, $char);
}
}
更多的优化
上述从需求至实现均仅于数小时内完成(但基本功能可用), 深知仍有很多不足及需要优化的地方. 期望大家指正及提出…
PS: 第一次发帖, 如有任何问题, 再次还望见谅… 如喜欢本文或上述代码对您有用, 烦请帮忙点赞以示支持, 十分感谢!!!