diff --git a/addons/shopro/controller/Ccblife.php b/addons/shopro/controller/Ccblife.php index bc1c40f..ea6be9b 100644 --- a/addons/shopro/controller/Ccblife.php +++ b/addons/shopro/controller/Ccblife.php @@ -137,7 +137,7 @@ class Ccblife extends Common * @param string $ccbUserId 建行用户ID * @param string $mobile 手机号 * @param string $openId OpenID - * @param array $params 其他参数 + * @param array $params 其他参数(包含 Usr_Name 等建行返回的数据) * @return array 用户信息 */ private function processUserLogin($ccbUserId, $mobile, $openId, $params) @@ -152,41 +152,67 @@ class Ccblife extends Common $isNew = false; Db::name('user')->where('id', $user['id'])->update([ + 'prevtime' => $user['logintime'] ?? null, // 记录上次登录时间 'logintime' => time(), 'loginip' => $this->request->ip(), 'updatetime' => time() ]); + // 重新获取更新后的用户信息 + $user = Db::name('user')->where('id', $user['id'])->find(); + } else { // 用户不存在,先尝试通过手机号查找 - if ($mobile) { + if (!empty($mobile)) { $user = Db::name('user')->where('mobile', $mobile)->find(); } if ($user) { - // 手机号已存在,更新建行用户ID + // 手机号已存在,绑定建行用户ID $isNew = false; Db::name('user')->where('id', $user['id'])->update([ 'ccb_user_id' => $ccbUserId, + 'prevtime' => $user['logintime'] ?? null, 'logintime' => time(), 'loginip' => $this->request->ip(), 'updatetime' => time() ]); + // 重新获取更新后的用户信息 + $user = Db::name('user')->where('id', $user['id'])->find(); + } else { // 创建新用户 $isNew = true; + // 从建行参数中获取用户名(Usr_Name 或 nickname) + $userName = $params['Usr_Name'] ?? $params['nickname'] ?? ''; + if (empty($userName)) { + $userName = '建行用户' . substr($ccbUserId, -4); + } + + // 生成盐值 + $salt = \fast\Random::alnum(); + $userData = [ 'ccb_user_id' => $ccbUserId, - 'username' => 'ccb_' . substr(md5($ccbUserId), 0, 8), - 'nickname' => $params['nickname'] ?? '建行用户' . substr($ccbUserId, -4), - 'mobile' => $mobile, + 'username' => 'ccb_' . substr(md5($ccbUserId), 0, 10), // 唯一用户名 + 'nickname' => $userName, + 'mobile' => $mobile ?: '', 'avatar' => $params['avatar'] ?? '/assets/img/avatar.png', 'status' => 'normal', - 'salt' => \fast\Random::alnum(), - 'password' => '', // 建行用户无需密码 + 'salt' => $salt, + 'password' => md5(md5(\fast\Random::alnum(32)) . $salt), // 随机密码 + 'group_id' => 0, // 默认用户组 + 'level' => 0, // 默认等级 + 'gender' => 0, // 未知性别 + 'money' => 0.00, + 'commission' => 0.00, + 'score' => 0, + 'successions' => 1, + 'maxsuccessions' => 1, + 'loginfailure' => 0, 'joinip' => $this->request->ip(), 'jointime' => time(), 'logintime' => time(), @@ -195,27 +221,45 @@ class Ccblife extends Common 'updatetime' => time() ]; - // 设置随机密码 - $userData['password'] = md5(md5(\fast\Random::alnum(32)) . $userData['salt']); + Log::info('创建建行新用户', [ + 'ccb_user_id' => $ccbUserId, + 'mobile' => $mobile, + 'nickname' => $userName + ]); $userId = Db::name('user')->insertGetId($userData); + if (!$userId) { + throw new \Exception('创建用户失败'); + } + $user = Db::name('user')->where('id', $userId)->find(); } } Db::commit(); + Log::info('建行用户登录处理成功', [ + 'user_id' => $user['id'], + 'ccb_user_id' => $ccbUserId, + 'is_new' => $isNew + ]); + return [ 'user_id' => $user['id'], 'nickname' => $user['nickname'], 'avatar' => $user['avatar'], - 'mobile' => $this->maskMobile($user['mobile']), + 'mobile' => $user['mobile'], 'is_new' => $isNew, 'ccb_user_id' => $ccbUserId ]; } catch (\Exception $e) { Db::rollback(); + Log::error('建行用户登录处理失败', [ + 'error' => $e->getMessage(), + 'ccb_user_id' => $ccbUserId, + 'trace' => $e->getTraceAsString() + ]); throw $e; } } @@ -283,19 +327,4 @@ class Ccblife extends Common $this->error('解密失败: ' . $e->getMessage()); } } - - /** - * 手机号脱敏 - * - * @param string $mobile 手机号 - * @return string 脱敏后的手机号 - */ - private function maskMobile($mobile) - { - if (empty($mobile) || strlen($mobile) !== 11) { - return ''; - } - - return substr($mobile, 0, 3) . '****' . substr($mobile, -4); - } } diff --git a/addons/shopro/library/ccblife/CcbUrlDecrypt.php b/addons/shopro/library/ccblife/CcbUrlDecrypt.php index 7005194..68bd92d 100644 --- a/addons/shopro/library/ccblife/CcbUrlDecrypt.php +++ b/addons/shopro/library/ccblife/CcbUrlDecrypt.php @@ -12,7 +12,14 @@ class CcbUrlDecrypt { /** * 解密建行URL参数ccbParamSJ - * 流程:URLDecode -> RSA解密(使用服务方私钥,内部会进行BASE64解码) + * + * 建行加密流程(参考Java demo): + * 1. 原文 → RSA公钥加密 → BASE64编码(第一次) + * 2. 再把结果转UTF-8字节 → BASE64编码(第二次) + * + * 解密流程: + * 1. URLDecode → BASE64解码(第一次)→ BASE64解码(第二次) + * 2. RSA私钥解密 → 原文 * * @param string $ccbParamSJ 加密的参数字符串(可能已经URLDecode) * @param string $privateKey 服务方私钥(BASE64格式或PEM格式) @@ -22,7 +29,7 @@ class CcbUrlDecrypt { try { // 调试日志 - trace('开始解密建行参数(RSA方式)', 'info'); + trace('开始解密建行参数(双重BASE64 + RSA)', 'info'); trace('ccbParamSJ 长度: ' . strlen($ccbParamSJ), 'info'); // 验证输入 @@ -34,12 +41,26 @@ class CcbUrlDecrypt throw new \Exception('privateKey 为空'); } - // URLDecode(如果还没有解码) + // 步骤1: URLDecode(如果还没有解码) $urlDecoded = urldecode($ccbParamSJ); trace('URLDecode后长度: ' . strlen($urlDecoded), 'info'); - // RSA解密(CcbRSA::decrypt内部会自动进行BASE64解码) - $decrypted = CcbRSA::decrypt($urlDecoded, $privateKey); + // 步骤2: 第一次 BASE64 解码 + $firstDecode = base64_decode($urlDecoded); + if ($firstDecode === false || empty($firstDecode)) { + throw new \Exception('第一次 BASE64 解码失败'); + } + trace('第一次BASE64解码成功,长度: ' . strlen($firstDecode), 'info'); + + // 步骤3: 第二次 BASE64 解码(建行特殊之处:双重BASE64编码) + $secondDecode = base64_decode($firstDecode); + if ($secondDecode === false || empty($secondDecode)) { + throw new \Exception('第二次 BASE64 解码失败'); + } + trace('第二次BASE64解码成功,长度: ' . strlen($secondDecode), 'info'); + + // 步骤4: RSA 私钥解密(直接处理二进制数据,不再用 CcbRSA::decrypt) + $decrypted = self::rsaPrivateDecrypt($secondDecode, $privateKey); if ($decrypted === false || empty($decrypted)) { throw new \Exception('RSA解密失败'); } @@ -47,7 +68,7 @@ class CcbUrlDecrypt trace('RSA解密成功,长度: ' . strlen($decrypted), 'info'); trace('解密内容: ' . $decrypted, 'info'); - // 解析参数字符串为数组 + // 步骤5: 解析参数字符串为数组 parse_str($decrypted, $params); if (empty($params)) { @@ -66,6 +87,58 @@ class CcbUrlDecrypt } } + /** + * RSA 私钥分块解密(不做 BASE64 解码) + * + * @param string $encryptedData 加密的二进制数据 + * @param string $privateKey 私钥(BASE64或PEM格式) + * @return string|false 解密后的数据 + */ + private static function rsaPrivateDecrypt($encryptedData, $privateKey) + { + try { + // 格式化私钥为 PEM 格式 + $pemPrivateKey = $privateKey; + if (strpos($pemPrivateKey, '-----BEGIN') === false) { + $pemPrivateKey = preg_replace('/\s+/', '', $pemPrivateKey); + $pemPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . + chunk_split($pemPrivateKey, 64, "\n") . + "-----END RSA PRIVATE KEY-----\n"; + } + + // 加载私钥资源 + $priKey = openssl_pkey_get_private($pemPrivateKey); + if (!$priKey) { + throw new \Exception('私钥格式错误: ' . openssl_error_string()); + } + + // 获取密钥大小 + $keyDetails = openssl_pkey_get_details($priKey); + $keySize = $keyDetails['bits'] / 8; // 1024位 = 128字节 + + // 分块解密 + $blocks = str_split($encryptedData, $keySize); + $decrypted = ''; + + foreach ($blocks as $block) { + $decryptedBlock = ''; + $success = openssl_private_decrypt($block, $decryptedBlock, $priKey, OPENSSL_PKCS1_PADDING); + if (!$success) { + throw new \Exception('RSA分块解密失败: ' . openssl_error_string()); + } + $decrypted .= $decryptedBlock; + } + + openssl_free_key($priKey); + + return $decrypted; + + } catch (\Exception $e) { + trace('RSA私钥解密错误: ' . $e->getMessage(), 'error'); + return false; + } + } + /** * DES解密 * 使用ECB模式,PKCS5Padding填充