前言
最近都在捣鼓我那个 筱锋工具箱(XF_TLS) 那个小玩意。因为包括了API啊登录,注册,注销用户等等操作。而且我想实现一端登录,另一端自动注销账号。所以随机Token验证相对来说就会重要很多。
本文全部内容均为原创!如需转载务必告知!请发件给 gm@x-lf.cn 联系
Copyright © 2016 - 2023 筱锋xiao_lfeng. All Rights Reserved.
构思
由于互联网的机制,在不使用 Websocket 的情况下,是不能够长时间与用户持续保持连接的。所以我这里打算使用 COOKIE 与数据库信息进行校验。 其中用户端储存的 Token 是经过 hash 加密过的数据,数据库存储的数据是不经过 hash 加密的原始数据。最终通过一系列计算后使用 PHP 自带的 hash 校验函数进行 Token 验证。
为什么我前面说实现一端登录,另一端自动注销呢? Token 在执行登陆之前,会生成初始 Token 数据,这部分数据是没有经过加密而且没有进行组合的,存储在 SESSION 中。在用户执行登陆时候,自行生成4位随机数做基础计算数据,随后检查目前服务器时间戳与用户端生成的 Token 数据进行比对,以及获取用户的连接 IP 地址解析坐标,再与 Token 内置提交的数据进行比对。最后将4位随机数,时间戳获取的关键数据,以及IP坐标数据,进行函数计算,得出的求余结果作为校验数。再将之前的数据与求余结果进行数据处理(注:如果求余是0则修改为其他数据),得出计算结果三位数。将所有数据按照事先规定好的规则排列成完整后形成用户 Token。
将上述生成的 Token 数据上传至服务器数据库,随后将 Token 数据进行加密,加入到用户浏览器的 COOKIE 中,在每次进入非登陆区域时候执行 Token 数据校验。符合则为正常用户,否则撤销用户的登录标识 COOKIE 数据并销毁 Token。
基于上述要求,如果对于登陆要求更加严格,可以设定在一个 IP 段内的登录是有效的,登陆有效期只有在一个市内(对于地理位置的相对确定,取决于地理位置API提供的坐标的精度是多少,国内收费IP定位精度可以到达区和部分指定地点,所以按需看,这里我打算设计精度以市计算,因为我自己的IP库中的精度就是市),由于跨端登录在登陆时候都会重新生成源 Token ,而这些 Token 永远不会与已生成的 Token 重复,所以当数据库更新后的 Token 检查与目前赋予用户端 COOKIE 值不符合则会撤销登录,那么另外一个客户端就可以正常登录,则前一个登陆的客户端会因为 Token 无法校验成功而撤销权限。
设计Token
请注意,我这里设计的 Token 都将会在 https://api.x-lf.cn/OpenSources/ 开放,故筱锋工具箱中的 Token 验证规则与我本文章写的 Token 验证规则是不同的,无法使用我开源后的 OpenSources 的 API 进行筱锋工具箱的所有操作!
- Token需要内容:
- 2位随机数
- 1位区分码(区分ipv4与ipv6)
- 十进制ipv4与十六进制ipv6(本博文设计 Token 为了方便展示,不进行数据压缩)
- 区域码
- 1位校验码
- 3位计算码(计算1,3,4,5结果求出的三位数)
- 进行数据举例:
- ipv4的数据:4501160251451880221145543
- 数据解析:[45][0][116025145188][022114][5][543]
- ipv6的数据:451240e04a13417cc5008845d3be99e01d40221145543
- 数据解析:[45][1][240e04a13417cc5008845d3be99e01d4][022114][5][543]
- ipv4的数据:4501160251451880221145543
定义
2位随机数
这里我们只允许两位随机数从1-7
代码语言:javascript复制/**
* @var int $i 循环数计算
* @var int $Cache_Data 循环取得单数缓存
* @var int $Token_RandData Token最终取得值
*/
for ($i=0; $i<=1; $i ) {
$Cache_Data = rand(1,7);
$Token_RandData = $Token_RandData.$Cache_Data;
}
1位区分码
由于 ipv4 的长度与 ipv6 的长度不相同,强制两个合并计算可能会导致最终的计算结果出现问题,最后的三位计算数无法计算出三位数或校验码等问题。 并且 INT 下强制使用ipv6可能会导致数据溢出计算错误。
代码语言:javascript复制/**
* @var int $Token_IpDistinguish Token取得IP区分码
*/
if (preg_match('/((2(5[0-5]|[0-4]d))|[0-1]?d{1,2})(.((2(5[0-5]|[0-4]d))|[0-1]?d{1,2})){3}/',$_SERVER["REMOTE_ADDR"])) {
$Token_IpDistinguish = 0;
} else {
// 由于 ipv6 的正则表达式较为复杂,通过使用 $_SERVER["REMOTE_ADDR"] 全局变量可以直接获取IP信息,如果是 ipv4 则会执行上面的条件,否则就是ipv6
$Token_IpDistinguish = 1;
}
十进制ipv4与十六进制ipv6
在这里我解释一下为什么我需要区分码:
首先,因为已经将数据格式化了,所以数据不再是符合ipv4及ipv6的正则表达式 在这里我可以放出来完整的ipv6的正则表达式
代码语言:javascript复制^([da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]d|[01]?dd?).){3}(25[0-5]|2[0-4]d|[01]?dd?)|::([da−fA−F]1,4:)0,4((25[0−5]|2[0−4]d|[01]?dd?).)3(25[0−5]|2[0−4]d|[01]?dd?)|::([da−fA−F]1,4:)0,4((25[0−5]|2[0−4]d|[01]?dd?).)3(25[0−5]|2[0−4]d|[01]?dd?)|^([da-fA-F]{1,4}:):([da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]d|[01]?dd?).){3}(25[0-5]|2[0-4]d|[01]?dd?)|([da−fA−F]1,4:)2:([da−fA−F]1,4:)0,2((25[0−5]|2[0−4]d|[01]?dd?).)3(25[0−5]|2[0−4]d|[01]?dd?)|([da−fA−F]1,4:)2:([da−fA−F]1,4:)0,2((25[0−5]|2[0−4]d|[01]?dd?).)3(25[0−5]|2[0−4]d|[01]?dd?)|^([da-fA-F]{1,4}:){3}:([da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]d|[01]?dd?).){3}(25[0-5]|2[0-4]d|[01]?dd?)|([da−fA−F]1,4:)4:((25[0−5]|2[0−4]d|[01]?dd?).)3(25[0−5]|2[0−4]d|[01]?dd?)|([da−fA−F]1,4:)4:((25[0−5]|2[0−4]d|[01]?dd?).)3(25[0−5]|2[0−4]d|[01]?dd?)|^([da-fA-F]{1,4}:){7}[da-fA-F]{1,4}|:((:[da−fA−F]1,4)1,6|:)|:((:[da−fA−F]1,4)1,6|:)|^[da-fA-F]{1,4}:((:[da-fA-F]{1,4}){1,5}|:)|([da−fA−F]1,4:)2((:[da−fA−F]1,4)1,4|:)|([da−fA−F]1,4:)2((:[da−fA−F]1,4)1,4|:)|^([da-fA-F]{1,4}:){3}((:[da-fA-F]{1,4}){1,3}|:)|([da−fA−F]1,4:)4((:[da−fA−F]1,4)1,2|:)|([da−fA−F]1,4:)4((:[da−fA−F]1,4)1,2|:)|^([da-fA-F]{1,4}:){5}:([da-fA-F]{1,4})?|([da−fA−F]1,4:)6:|([da−fA−F]1,4:)6:
对于获取来讲可以完全没必要,使用 $_SERVER["REMOTE_ADDR"]
全局变量来说,无非就是 ipv4 和 ipv6 那么直接判断 ipv4 ,而剩下的就是 ipv6 。
随后由于最终我们组合或拆分 Token 都是需要进行的,所以相比于再次使用一次 strlen 变量,加一位区分码可以优化解析速度(实际运算中无特别大区别,两者皆可)。 而且代码的观感会更好一些。下面做出对比
代码语言:javascript复制// 使用区分码
/**
* @var string $Result_Token_Object->Token 数据库获取Token
* @var array $Token Token进行拆分后组成数组
*/
// 分割数据
$Token['Rand'] = (int)($Result_Token_Object->Token,0,2);
$Token['IpDistinguish'] = (int)substr($Result_Token_Object->Token,2,1);
if ($Token['IpDistinguish'] == 0) {
// ipv4
$Token['ip'] = substr($Result_Token_Object->Token,3,12);
$Token['place'] = (int)substr($Result_Token_Object->Token,15,6);
$Token['check'] = (int)substr($Result_Token_Object->Token,21,1);
$Token['calc'] = (int)substr($Result_Token_Object->Token,22,3);
} else {
// ipv6
$Token['ip'] = substr($Result_Token_Object->Token,3,30);
$Token['place'] = (int)substr($Result_Token_Object->Token,33,6);
$Token['check'] = (int)substr($Result_Token_Object->Token,39,1);
$Token['calc'] = (int)substr($Result_Token_Object->Token,40,3);
}
代码语言:javascript复制// 不使用区分码
/**
* @var string $Result_Token_Object->Token 数据库获取Token
* @var array $Token Token进行拆分后组成数组
*/
// 分割数据
$Token['Rand'] = (int)substr($Result_Token_Object->Token,0,2);
if (strval((string)$Result_Token_Object->Token) == 25) { // 检查位数,如果是25位则是ipv4如果是ipv6则是43位
// ipv4
$Token['ip'] = substr($Result_Token_Object->Token,2,12);
$Token['place'] = (int)substr($Result_Token_Object->Token,14,6);
$Token['check'] = (int)substr($Result_Token_Object->Token,20,1);
$Token['calc'] = (int)substr($Result_Token_Object->Token,21,3);
} else {
// ipv6
$Token['ip'] = substr($Result_Token_Object->Token,2,30);
$Token['place'] = (int)substr($Result_Token_Object->Token,32,6);
$Token['check'] = (int)substr($Result_Token_Object->Token,38,1);
$Token['calc'] = (int)substr($Result_Token_Object->Token,39,3);
}
代码语言:javascript复制// 代码精简化
/**
* @var string $Result_Token_Object->Token 数据库获取Token
* @var array $Token Token进行拆分后组成数组
*/
// 分割数据
$Token = [
'Rand'=>(int)($Result_Token_Object->Token,0,2),
'IpDistinguish'=>(int)substr($Result_Token_Object->Token,2,1),
'ip'=>substr($Result_Token_Object->Token,3,Ip_Long($Token['IpDistinguish'])),
'place'=>(int)substr($Result_Token_Object->Token,(3 Ip_Long($Token['IpDistinguish'])),6),
'check'=>(int)substr($Result_Token_Object->Token,(9 Ip_Long($Token['IpDistinguish'])),1),
'calc'=>(int)substr($Result_Token_Object->Token,(10 Ip_Long($Token['IpDistinguish'])),3),
];
/**
* @var string $IpDistinguish 获取IP区分码
* @return int
*/
function Ip_Long($IpDistinguish) {
if ($IpDistinguish == 0) return 12; else return 30;
}
以上部分就是说对于 Token 的解码操作所对应的操作,为了方便解码所以我们在进行编码的时候多新增一位符号作为判断符,简化运算过程。 下面获取ipv4或ipv6的方式
代码语言:javascript复制/**
* @var int $Token_IpDistinguish Token取得IP区分码
* @var array $Array_IpAddress 获取抛开符号的IP地址
* @var array $Array_PutIn 补充数组
* @var int $i,$j 循环位数
* @var string $Token_IP Token取得IP
*/
if (!$Token_IpDistinguish) {
$Array_IpAddress = explode('.',$_SERVER["REMOTE_ADDR"]);
for ($i=0; $i<=3; $i ) {
if (strlen((int)$Array_IpAddress[$i]) != 3) $Array_IpAddress[$i] = Repair0(3-strlen((int)$Array_IpAddress[$i])).$Array_IpAddress[$i];
}
} else {
$Array_IpAddress = explode(':',$_SERVER["REMOTE_ADDR"]);
for ($i=0; $i<=7; $i ) {
if (empty($Array_IpAddress[$i])) {
if (!empty($Array_IpAddress[$i 1])) continue; else break;
} else {
if (strval($Array_IpAddress[$i]) != 4) $Array_IpAddress[$i] = Repair0(4-strval($Array_IpAddress[$i])).$Array_IpAddress[$i];
}
}
// 补充位数
for ($i=0; $i<=count($Array_IpAddress)-1; $i ) {
if (in_array(null,$Array_IpAddress)) {
for ($j=0; $j<=7-count($Array_IpAddress); $j ) {
$Array_PutIn[$j] = 0000;
}
for ($j=0; $j<=7-count($Array_IpAddress); $j ) {
if ($Array_IpAddress[$j] == null) break;
}
array_splice($Array_IpAddress,($j 1),0,$Array_PutIn);
}
}
}
$Token_IP = implode(null,$Array_IpAddress);
/**
* @param $long int 补充长度单位
* @param $i int 循环位数
* @return int
*/
function Repair0 ($long) {
for ($i=1; $i<=$long; $i ) return 0;
}
区域码
区域码需要借助外部IP库,在这里我用的是我自己的IP库。 调用地址 https://api.x-lf.cn/ip/select/
请求方式 GET
- 参数
- ukey String 个人用户密钥(在 https://center.x-lf.cn/ 获取)
- type String 切换ipv4或ipv6
- ip String IP信息
详细信息请查看开发文档(啊,我还没写,写完再来更新这一段)
返回示例
代码语言:javascript复制{
"output": "SUCCESS",
"code": 200,
"info": "查询成功",
"data": {
"ip": "000.000.000.000",
"country_sx": "CN",
"country": "China",
"province": "Guangdong",
"city": "Shenzhen",
"longitude": "22.545540",
"latitude": "114.068300",
"post_code": "518026",
"time_zone": " 08:00"
}
}
代码语言:javascript复制/**
* @var array $ApiIP 从IP库获取信息
* @var int $Token_Place Token区域码
*/
// 获取IP等参数信息
$ApiIP_url = 'https://api.x-lf.cn/ip/select/?ukey=XFUKEY00000000000000&type=ipv4&ip='.$_SERVER["REMOTE_ADDR"];
$ApiIP_ch = curl_init($ApiIP_url);
curl_setopt($ApiIP_ch,CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);
curl_setopt($ApiIP_ch, CURLOPT_RETURNTRANSFER, true);
$ApiIP = curl_exec($ApiIP_ch);
$ApiIP = json_decode($ApiIP,true);
if (strlen(intval($ApiIP['data']['longitude'])) != 3) {
$ApiIP['data']['longitude'] = Repair0(3-strlen(intval($ApiIP['data']['longitude']))).$ApiIP['data']['longitude'];
}
if (strlen(intval($ApiIP['data']['latitude'])) != 3) {
$ApiIP['data']['latitude'] = Repair0(3-strlen(intval($ApiIP['data']['latitude']))).$ApiIP['data']['latitude'];
}
$Token_Place = Repair0(6-strlen(intval($ApiIP['data']['longitude']).intval($ApiIP['data']['latitude']))).intval($ApiIP['data']['longitude']).intval($ApiIP['data']['latitude']);
/**
* @param $long int 补充长度单位
* @param $i int 循环位数
* @return int
*/
function Repair0 ($long) {
for ($i=1; $i<=$long; $i ) return 0;
}
1位校验码
求余校验码,在这里只是作为以防别人篡改数据的第一个屏障,本文构建的校验码并未考虑其合理性,所以可能被恶意修改某个值后求余的值依旧相同的。 在这里使用ip所对应的十进制数取得后设置为整形变量【int】,使用此变量除以区域码加上随机数除8求余 (这里不讲究逻辑配置,只简单说明校验码作用)
使用此例子
- ipv4的数据:4501160251451880221145543
- 数据解析:[45][0][116025145188][022114][5][543]
// 下面数字为例子
$Token_Rand = 45:
$Token_IpDistinguish = 0;
$Token_IP = 116025145188;
$Token_Place = 022114;
/**
* @var int $Token_Check Token取得校验码
*/
// 逻辑计算
if ($Token_IpDistinguish) {
// 匹配ipv6下全部符合0-9的字符并组合后强制转译成整形变量
preg_match_all('/[0-9] /',$Token_IP,$Cache_IP);
$Token_IP = substr(implode(null,$Cache_IP[0]),0,12);
}
$Token_Check = round($Token_IP / $Token_Place $Token_RandData) % 8;
3位计算码
计算码,为了博文描述方便,这里使用求出的值只取前三位数字即可! 使用ip所对应的十进制数取得后设置为整形变量【int】,使用此变量除以区域码加上随机数,求得结果获取最后三位数。
代码语言:javascript复制$Token_Calc = substr(round($Token_IPs / $Token_Place $Token_RandData),-3,3);
完整代码
代码语言:javascript复制// 算法构建
for ($i=0; $i<=1; $i ) {
$Cache_Data = rand(1,7);
$Token_RandData = $Token_RandData.$Cache_Data;
}
if (preg_match('/((2(5[0-5]|[0-4]d))|[0-1]?d{1,2})(.((2(5[0-5]|[0-4]d))|[0-1]?d{1,2})){3}/',$_SERVER["REMOTE_ADDR"])) {
$Token_IpDistinguish = 0;
} else {
$Token_IpDistinguish = 1;
}
if (!$Token_IpDistinguish) {
$Array_IpAddress = explode('.',$_SERVER["REMOTE_ADDR"]);
for ($i=0; $i<=3; $i ) {
if (strlen((int)$Array_IpAddress[$i]) != 3) $Array_IpAddress[$i] = Repair0(3-strlen((int)$Array_IpAddress[$i])).$Array_IpAddress[$i];
}
} else {
$Array_IpAddress = explode(':',$_SERVER["REMOTE_ADDR"]);
for ($i=0; $i<=7; $i ) {
if (empty($Array_IpAddress[$i])) {
if (!empty($Array_IpAddress[$i 1])) continue; else break;
} else {
if (strlen($Array_IpAddress[$i]) < 4) $Array_IpAddress[$i] = Repair0(4-strlen($Array_IpAddress[$i])).$Array_IpAddress[$i];
}
}
// 补充位数
for ($i=0; $i<=count($Array_IpAddress)-1; $i ) {
if (in_array(null,$Array_IpAddress)) {
for ($j=0; $j<=7-count($Array_IpAddress); $j ) {
$Array_PutIn[$j] = 0000;
}
for ($j=0; $j<=7-count($Array_IpAddress); $j ) {
if ($Array_IpAddress[$j] == null) break;
}
array_splice($Array_IpAddress,($j 1),0,$Array_PutIn);
}
}
}
$Token_IP = implode(null,$Array_IpAddress);
// 获取IP等参数信息
$ApiIP_url = 'https://api.x-lf.cn/ip/select/?ukey=0000000000fhvgb2tyar&type=ipv4&ip='.$_SERVER['REMOTE_ADDR'];
$ApiIP_ch = curl_init($ApiIP_url);
curl_setopt($ApiIP_ch,CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT']);
curl_setopt($ApiIP_ch, CURLOPT_RETURNTRANSFER, true);
$ApiIP = curl_exec($ApiIP_ch);
$ApiIP = json_decode($ApiIP,true);
if (strlen(intval($ApiIP['data']['longitude'])) != 3) {
$ApiIP['data']['longitude'] = Repair0(3-strlen(intval($ApiIP['data']['longitude']))).$ApiIP['data']['longitude'];
}
if (strlen(intval($ApiIP['data']['latitude'])) != 3) {
$ApiIP['data']['latitude'] = Repair0(3-strlen(intval($ApiIP['data']['latitude']))).$ApiIP['data']['latitude'];
}
$Token_Place = Repair0(6-strlen(intval($ApiIP['data']['longitude']).intval($ApiIP['data']['latitude']))).intval($ApiIP['data']['longitude']).intval($ApiIP['data']['latitude']);
// 逻辑计算
if ($Token_IpDistinguish) {
// 匹配ipv6下全部符合0-9的字符并组合后强制转译成整形变量
preg_match_all('/[0-9] /',$Token_IP,$Cache_IP);
$Token_IPs = substr(implode(null,$Cache_IP[0]),0,12);
}
$Token_Check = round($Token_IPs / $Token_Place $Token_RandData) % 8;
$Token_Calc = substr(round($Token_IPs / $Token_Place $Token_RandData),-3,3);
$Token = $Token_RandData.$Token_IpDistinguish.$Token_IP.$Token_Place.$Token_Check.$Token_Calc;
结果
暂不清楚,没有做解开Token相关操作。 对于可行性来说是完全可行的。
对于解析说明。解析不需要重置相关随机数。只需Token中所需的数据进行操作处理,我们需要验证地址,只需要验证Token内容后提取出ipv4或ipv6的内容段即可。 对于验证IP段也只需要取部分数据即可,不需要验证全部数据即可实现IP段验证。 地域验证就在后面的区域码,当跨市或所在的IP不是一个市内的其经纬度都将会进行变化(经纬度指向每个市的市中心),则变化后,数据也造成不符合。
那需要每次用户端都生成一个 Token 吗? 不需要,在我这里的构思中,是不需要用户端重复生成 Token ,只有在登陆时候需要用户生成 Token ,而其他只需要将 Token 以加密形式存储在用户的 COOKIE 中即可。 请注意!本博文是为了解释我的构思,将我的 Token 想法构建了出来。
在实际生产环境中, Token 算法请勿泄露,否则会很大概率提升被篡改的风险!
后言
参考
无!全篇根据已有知识自行构思。
工具
如果你想使用我开源API中的调用数据,请先进入筱锋工具箱申请开源通讯密钥
使用接口需要 okey
晓白云图床 筱锋工具箱