Files
wenquanauth/api/Communication.php
youpaishidifu 24264cf320 加固-06(P0-05): 密码全部以 bcrypt 哈希入库,旧格式(明文/MD5)兼容并惰性迁移
背景:sq_user/sq_agent 密码明文入库、sq_admin 存 MD5;订单表 sq_trade.pass 明文在途;
找回密码 API 会把明文密码直接发邮件;日志记录明文密码。数据库泄露 = 全部账号密码泄露。

1. function/function_core.php 新增 3 个 helper(updateset 之后):
   - wq_is_hashed_password($v):判断存储值是否为新 bcrypt 哈希(60 长度 $2y$ 前缀)
   - wq_hash_password($plain):password_hash(PASSWORD_BCRYPT)
   - wq_verify_password($plain,$stored,$legacy):兼容 新 bcrypt / 旧明文(sq_user/sq_agent) / 旧 md5(sq_admin),
     哈希比对一律 hash_equals,恒定时间比较防时序侧信道

2. 所有落库点改为 bcrypt:
   - 用户开户漏斗 function/function_auth.php auth_add 入口幂等补哈希(上游已哈希则原样保留)
   - 代理开通漏斗 function/trade.inc.php type=4 代理落库前幂等补哈希
   - 下单点订单密码哈希入库:ajax.php(320/388/781)、agent/ajax.php(439) —— 订单表不再有明文在途密码
   - admin/ajax.php:addagent、addadmin、changeagent(mod=password)、changepassword 新密码
   - agent/ajax.php:开下级代理、changeuser(mod=password)、changeagent(mod=password)、changepassword 新密码
   - api.php:ChangePassword 写库
   - api/Communication.php 与 function/communication.php:用户登录校验成功后,旧明文惰性升级为 bcrypt

3. 所有校验点改为取行 + PHP 校验(不再 WHERE password=明文):
   - admin 登录(旧 MD5 兼容,成功即写回 bcrypt,session 与 DB 存同一新哈希保住 re-check 机制)
   - agent 密码登录(旧明文兼容,惰性迁移);agent token 登录 session 改存 DB 存储值
   - 根 ajax.php 续费 kt 密码校验(379/773)、卡密找回密码(1002)
   - api.php GetUserInfo 内联校验、getuserinfo() 校验(均带惰性迁移)

4. 找回密码改造(api.php RetrievePassword):
   密码已不可逆,原"邮件下发原密码"改为"重置为一次性随机新密码(bin2hex(random_bytes(6)))+ 哈希落库 + 仅邮件投递一次"

5. 日志/回传去明文:
   - admin 登录失败日志不再记录明文密码(改记账号)
   - admin getagentinfo 不再回传密码值(掩码 ********,经确认 UI 无回写风险)

6. 表结构:sq_admin.password varchar(33) -> varchar(255)(bcrypt 60 字符装不下)
   - wenquan_auth.sql 同步更新(新装库直接可用)
   - 已有库需执行:ALTER TABLE sq_admin MODIFY password varchar(255) NOT NULL;(本次本地库已执行)

回归(40 项全 PASS,8082 内置服务器 + 8081 nginx 双通道冒烟):
- API(c=api) 旧明文用户登录成功即升级 bcrypt;再登录幂等不错哈希;错密码 -205 被拒
- admin 旧 MD5 登录即迁移 bcrypt;再登录/错密码/日志无明文/checklogin 会话重校验均通过
- admin 新增管理员、改密(旧密码失效/新密码生效/恢复基线)
- admin 新增代理直接 bcrypt;getagentinfo 掩码;changeagent 改密哈希入库
- 旧明文代理登录即迁移 bcrypt;checklogin 通过;代理改密旧失效新生效
- admin 手工开通(funnel)明文 POST 直接存 bcrypt;API 登录新开通用户成功
- 注:测试应用 free 必须为 0(free=1 时 Login 入口直接放行,属产品设计,不走密码校验)

已知行为变化(记录):用户/代理列表按"密码"关键词搜索不再命中(存储已哈希,搜索本身即泄露向量,收紧);
列表中密码列显示 60 位哈希串而非明文;改密会使该账号现有会话失效需重新登录(与旧行为一致)。

api.php 为 2024 树中的旧入口(已被 api/Communication.php 取代),仍按同口径同步修了 4 处密码点防复活踩坑。
2026-10-10 12:25:52 +08:00

247 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Created by PhpStorm.
* User: 温泉
* Date: 2018/7/9
* Time: 17:31
* Notice: 强加密版Api,与易语言通信
*/
error_reporting(1); //只输出致命错误
include __DIR__ . '/../function/function_core.php';
include __DIR__ . '/../function/function_app.php';
include __DIR__ . '/../function/Tips.php';
if (!empty($_GET['c']) && $_GET['c'] == 'api') include __DIR__ . '/../function/api.class.php';else include __DIR__ . '/../function/Encryption.class.php';
if (empty($_GET['appid'])) die(makejson(-1,$Tips['NoAppID']));
if (strlen($_GET['sign']) !== 32) die(makejson(-2,$Tips['NoSign']));
$data = $_POST['data'];
if (!$appinfo = app_idgetinfo($_GET['appid'])) die(makejson(-4,$Tips['GetAppInfoErr']));
$communication = !empty($_GET['c']) && $_GET['c'] == 'api'? new ApiCommunication($appinfo['connectkey'],$appinfo['decryptkey']) : new communication($appinfo['connectkey'],$appinfo['decryptkey']);
if ($communication->getsign($data) !== $_GET['sign']) die(makejson(-3,$Tips['SignError']));
$time = time();
$d = !empty($_GET['c']) && $_GET['c'] == 'api' ? $data : $communication->data_decode($data);
unset($data);
$data = json_decode($d,true);
$sign = '';
if ($data['mod'] === 'Initialization'){
if ($data['ddition'] < $appinfo['ver'] && numtobool($appinfo['forceup'])) $communication->back(-101,'AppVerOld',array('updateurl'=>$appinfo['upurl'],'updatelog'=>$appinfo['uplog'],'notice'=>$appinfo['notice']));
if (numtobool($appinfo['close'])) $communication->back(-102,'AppClosed',array('updateurl'=>$appinfo['upurl'],'updatelog'=>$appinfo['uplog'],'notice'=>$appinfo['notice']));
$token = rand_str(64);
$db->insert_back_id('sq_token',array('token'=>$token,'ip'=>get_real_ip(),'addtime'=>$time,'appid'=>$_GET['appid']));
$communication->back(1,'',array('updateurl'=>$appinfo['upurl'],'updatelog'=>$appinfo['uplog'],'notice'=>$appinfo['notice'],'token'=>$token));
}
if ($data['mod'] === 'Login'){
//echo 'login';
if (empty($data['token']) || !$tokeninfo = $db->select_first_row('sq_token','uid,ip',array('token'=>$data['token']),'AND')) $communication->back(-201,'InitTimeOut');
$userip = empty($data['ip']) ? get_real_ip() : $data['ip'];
if ($appinfo['free']) {
$db->update('sq_token',array('token'=>$data['token']),'AND',array('uid'=>0,'start'=>time()));
$communication->back(1);
}
if ($appinfo['logintype'] == 'zhmm') {
if (empty($data['user'])) $communication->back(-202,'NoUsername');
if (empty($data['pass'])) $communication->back(-203,'NoPassword');
if (!$userinfo = $db->select_first_row('sq_user', '*', array('appid' => $_GET['appid'], 'username' => $data['user']), 'AND')) $communication->back(-204,'ErrorUsername');
// P0-05: 兼容旧明文 / 新 bcrypt 校验用户密码;旧明文校验成功后惰性升级为 bcrypt
if (!wq_verify_password($data['pass'], $userinfo['password'])) $communication->back(-205,'ErrorPassword');
if (!wq_is_hashed_password($userinfo['password'])) {
$db->update('sq_user', array('ID' => $userinfo['ID']), 'AND', array('password' => wq_hash_password($data['pass'])));
}
} else if ($appinfo['logintype'] == 'kmsq') {
if (empty($data['user'])) $communication->back(-211,'NoKami');
if (!$userinfo = $db->select_first_row('sq_user', '*', array('appid' => $_GET['appid'], 'username' => $data['user']), 'AND')) $communication->back(-212,'ErrorKami');
} else if ($appinfo['logintype'] == 'jcbd') {
$where['appid'] = $_GET['appid'];
if ($appinfo['bindip'] == 1) $where['lip'] = $userip;
if ($appinfo['bindmac'] == 1) $where['mac'] = $data['mac'];
if ($appinfo['bindqq'] == 1) $where['rqq'] = $data['robot'];
if (!$userinfo = $db->select_first_row('sq_user', '*', $where, 'AND')) $communication->back(-221,'ErrorBind');
}
if ($userinfo['status'] != 1) $communication->back(-231,'UserCold');
if ($appinfo['usetype'] == 'dqsj') {
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1') $communication->back(-232,'AuthTimeOut',array('balance'=>$userinfo['balance']));
} else if ($appinfo['usetype'] == 'kcye') {
if ($userinfo['balance'] <= 0) $communication->back(-233,'BalanceOut');
} else {
$communication->back(-4,'GetAppInfoErr');
}
$bindip = $userinfo['lip'];
$bindmac = $userinfo['mac'];
$usermac = $data['mac'];
$bindrqq = $userinfo['rqq'];
$userqq = $data['robot'];
if ($appinfo['bindip'] == 1 && !empty($userinfo['lip']) && $userinfo['lip'] !== $userip) {
$communication->back(-234,'IpBindNeed',array('bindip'=>$userinfo['lip']));
}
if ($appinfo['bindmac'] == 1 && !empty($userinfo['mac']) && $bindmac !== $usermac) {
$communication->back(-235,'MacBindNeed',array('bindmac'=>$userinfo['mac']));
}
if ($appinfo['bindqq'] == 1 && !empty($userinfo['rqq']) && $bindrqq !== $userqq) {
$communication->back(-236,'QQBindNeed',array('bindqq'=>$userinfo['rqq']));
}
$db->update('sq_user', array('ID' => $userinfo['ID']), 'AND', array('rqq' =>$userqq, 'mac' =>$usermac, 'lip' => $userip, 'ltime' => time()));
//$token = rand_str(64);
$db->update('sq_token',array('token'=>$data['token']),'AND',array('uid'=>$userinfo['ID'],'start'=>time()));
//$db->insert_back_id('sq_token',array('token'=>$token,'uid'=>$userinfo['ID'],'lastest'=>$time,'start'=>$time));
if ($appinfo['onlinecheck'] == 1) {
$onlineuser = $db->select_all_row('sq_token','lastest,ip',array('uid'=>$userinfo['ID']),'AND','ORDER BY lastest DESC');
foreach ($onlineuser as $on){
if ((time() - $on['lastest']) < $appinfo['onlinesecond']){
$communication->back(-301,'LoggedInTips',array('ip'=>$on['ip'],''));
}else{
break;
}
}
}
$communication->back(1);
}
if ($data['mod'] === 'GoOnline'){
if (empty($data['token']) || !$tokeninfo = $db->select_first_row('sq_token','*',array('token'=>$data['token']),'AND')) $communication->back(-201,'InitTimeOut');
if ($tokeninfo['start'] == 0 || empty($tokeninfo['start'])){
$communication->back(-201,'InitTimeOut');
}
if ($appinfo['free']) {
$db->update('sq_token',array('token'=>$data['token']),'AND',array('lastest'=>time()));
$communication->back(1);
}
if ($appinfo['onlinecheck'] == 1){
$db->delete('sq_token',array('uid'=>$tokeninfo['uid']),'AND');
}
//$db->insert_back_id('sq_token',array('token'=>$tokeninfo['token'],'uid'=>$tokeninfo['uid'],'start'=>$tokeninfo['start'],'lastest'=>time()));
$communication->back(1);
}
if ($data['mod'] === 'OnlineHeart'){
if (empty($data['token'])) $communication->back(-401,'HeartNoToken');
if (!$tokeninfo = $db->select_first_row('sq_token','*',array('token'=>$data['token']),'AND')) $communication->back(-402,'HeartTokenError');
if ($tokeninfo['start'] == 0 || empty($tokeninfo['start'])){
$communication->back(-201,'InitTimeOut');
}
$db->update('sq_token',array('token'=>$data['token']),'AND',array('lastest'=>time()));
if ($appinfo['free']) {
$communication->back(1,'',array('time'=>$time));
}
if (!$userinfo = $db->select_first_row('sq_user','*',array('ID'=>$tokeninfo['uid']),'AND')) $communication->back(-234,'HeartNoFoundUser');
if ($userinfo['status'] != 1) $communication->back(-231,'UserCold');
if ($appinfo['usetype'] == 'dqsj') {
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1') $communication->back(-232,'AuthTimeOut');
} else if ($appinfo['usetype'] == 'kcye') {
if ($userinfo['balance'] <= 0) $communication->back(-233,'BalanceOut');
} else {
$communication->back(-4,'GetAppInfoErr');
}
$msg = '';
if ($appinfo['usetype'] == 'kcye') {
$time = time();
$chazhi = time() - $userinfo['lastreducetime'];
if ($chazhi > $appinfo['onlinesecond'] + 120) {
//新登录
$lastbalance = (int)$userinfo['balance'] - (int)$appinfo['cycreduce'];
$lastreducetime = $time;
$msg = '余额扣除成功';
} else {
//在线减
$oninemin = (int)($chazhi / 60);
$lastbalance = $userinfo['balance'] - $appinfo['cycreduce'] * $oninemin;
$lastreducetime = $userinfo['lastreducetime'] - $oninemin * 60;
$msg = '余额扣除成功';
}
$db->update('sq_user', array('ID' => $userinfo['ID'], 'appid' => $_GET['appid']), 'AND', array('htime' => time(), 'balance' => $lastbalance, 'lastreducetime' => $lastreducetime));
} else {
$db->update('sq_user', array('ID' => $userinfo['ID'], 'appid' => $_GET['appid']), 'AND', array('htime' => time()));
}
$communication->back(1,'',array('time'=>$time,'o'=>$msg));
}
if ($data['mod'] == 'GetCustumData'){
if (empty($data['token'])) $communication->back(-401,'HeartNoToken');
if (!$tokeninfo = $db->select_first_row('sq_token','*',array('token'=>$data['token']),'AND')) $communication->back(-402,'HeartTokenError');
if ($tokeninfo['start'] == 0 || empty($tokeninfo['start'])){
$communication->back(-201,'InitTimeOut');
}
if ($appinfo['free']) {
$communication->back(1,'',array('data'=>$appinfo['data']));
}
if (!$userinfo = $db->select_first_row('sq_user','*',array('ID'=>$tokeninfo['uid']),'AND')) $communication->back(-234,'HeartNoFoundUser');
if ($userinfo['status'] != 1) $communication->back(-231,'UserCold');
if ($appinfo['usetype'] == 'dqsj') {
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1') $communication->back(-232,'AuthTimeOut');
} else if ($appinfo['usetype'] == 'kcye') {
if ($userinfo['balance'] <= 0) $communication->back(-233,'BalanceOut');
} else {
$communication->back(-4,'GetAppInfoErr');
}
$communication->back(1,'',array('data'=>$appinfo['data']));
}
if ($data['mod'] == 'GetAppInfo'){
if (empty($data['token']) || !$tokeninfo = $db->select_first_row('sq_token','uid',array('token'=>$data['token']),'AND')) $communication->back(-201,'InitTimeOut');
$bool = true;
$i = 0 ;
while ($bool){
if (isset($appinfo[$i])){
unset($appinfo[$i]);
}else{
$bool = false;
}
$i ++ ;
}
// 安全加固 (P0-04):connectkey/decryptkey 是服务端与 SDK 通信的签名/解密密钥,
// 原实现把 sq_apps 全行(含这两个密钥明文)返回给 SDK,导致任何持有合法
// token 的调用方/抓包者拿到密钥后可伪造该应用的任意 API 请求。改为掩码下发。
if (isset($appinfo['connectkey'])) $appinfo['connectkey'] = str_repeat('*', 8);
if (isset($appinfo['decryptkey'])) $appinfo['decryptkey'] = str_repeat('*', 8);
$communication->back(1,'',$appinfo);
}
if ($data['mod'] == 'GetUserInfo'){
if (empty($data['token'])) $communication->back(-401,'HeartNoToken');
if (!$tokeninfo = $db->select_first_row('sq_token','*',array('token'=>$data['token']),'AND')) $communication->back(-402,'HeartTokenError');
if ($tokeninfo['start'] == 0 || empty($tokeninfo['start'])){
$communication->back(-201,'InitTimeOut');
}
if ($appinfo['free']) {
$communication->back(2,'');
}
if (!$userinfo = $db->select_first_row('sq_user','*',array('ID'=>$tokeninfo['uid']),'AND')) $communication->back(-234,'HeartNoFoundUser');
if ($userinfo['status'] != 1) $communication->back(-231,'UserCold');
if ($appinfo['usetype'] == 'dqsj') {
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1') $communication->back(-232,'AuthTimeOut');
} else if ($appinfo['usetype'] == 'kcye') {
if ($userinfo['balance'] <= 0) $communication->back(-233,'BalanceOut');
} else {
$communication->back(-4,'GetAppInfoErr');
}
$communication->back(1,'',$userinfo);
}
if($data['mod'] == 'OffLine'){
if (empty($data['token'])) $communication->back(-401,'HeartNoToken');
if (!$tokeninfo = $db->select_first_row('sq_token','*',array('token'=>$data['token']),'AND')) $communication->back(-402,'HeartTokenError');
$db->delete('sq_token',array('token'=>$data['token']),'AND');
$communication->back(1);
}