diff --git a/addons/shopro/library/ccblife/CcbEncryption.php b/addons/shopro/library/ccblife/CcbEncryption.php index 7a0ee64..c3efbd9 100644 --- a/addons/shopro/library/ccblife/CcbEncryption.php +++ b/addons/shopro/library/ccblife/CcbEncryption.php @@ -7,12 +7,19 @@ use think\Exception; /** * 建行生活加密解密核心类 * - * 功能: - * - RSA加密与解密 - * - MD5签名生成与验证 - * - 报文构造与解析 - * - 交易流水号生成 + * ⚠️ 已废弃:请使用 CcbRSA、CcbMD5、CcbHttpClient 类替代 * + * 废弃原因: + * 1. formatKey() 方法存在密钥格式化错误(PKCS#1 vs PKCS#8 混淆) + * 2. chunk_split() 使用不当导致 OpenSSL ASN1 解析错误 + * 3. 与 CcbRSA 类功能重复,维护成本高 + * + * 迁移指南: + * - RSA加密/解密 → 使用 CcbRSA::encrypt() / CcbRSA::decrypt() + * - MD5签名 → 使用 CcbMD5::signApiMessage() / CcbMD5::verifyApiSignature() + * - 加密商户公钥 → 在 CcbPaymentService 中使用 encryptPublicKeyLast30() + * + * @deprecated 2025-01-21 统一使用 CcbRSA、CcbMD5 类 * @author Billy * @date 2025-01-16 */ diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 1a4427f..8352489 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -34,50 +34,13 @@ class CcbOrderService throw new \Exception('建行生活配置文件不存在'); } - // 处理BASE64格式的密钥 - $this->config = $this->processPemKeys($this->config); + // ✅ 修复: 删除processPemKeys()调用 + // 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误 + // CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式 $this->httpClient = new CcbHttpClient($this->config); } - /** - * 处理PEM格式密钥 - * - * @param array $config - * @return array - */ - private function processPemKeys($config) - { - if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) { - $config['private_key'] = "-----BEGIN PRIVATE KEY-----\n" - . chunk_split($config['private_key'], 64, "\n") - . "-----END PRIVATE KEY-----"; - } - - if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) { - $config['public_key'] = "-----BEGIN PUBLIC KEY-----\n" - . chunk_split($config['public_key'], 64, "\n") - . "-----END PUBLIC KEY-----"; - } - - if (empty($config['merchant_public_key'])) { - $config['merchant_public_key'] = $config['public_key']; - } - - // 处理平台公钥 - if (!empty($config['platform_public_key'])) { - if (strpos($config['platform_public_key'], '-----BEGIN') === false) { - $config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n" - . chunk_split($config['platform_public_key'], 64, "\n") - . "-----END PUBLIC KEY-----"; - } - } else { - $config['platform_public_key'] = $config['public_key']; - } - - return $config; - } - /** * 推送订单到建行生活平台 * 当用户下单后调用此方法同步订单信息 @@ -112,23 +75,10 @@ class CcbOrderService // 获取订单商品列表 $orderItems = Db::name('shopro_order_item') ->where('order_id', $orderId) - ->select() - ->toArray(); - - // 获取支付流水号(PAY_FLOW_ID必填字段) - $payInfo = Db::name('shopro_pay') - ->where('order_id', $orderId) - ->where('status', 'paid') - ->find(); - - if (!$payInfo || empty($payInfo['pay_sn'])) { - throw new \Exception('订单未支付或支付流水号不存在'); - } - - $payFlowId = $payInfo['pay_sn']; + ->select(); // 构建订单数据(符合A3341TP01接口规范) - $orderData = $this->buildOrderData($order, $orderItems, $ccbUserId, $payFlowId); + $orderData = $this->buildOrderData($order, $orderItems, $ccbUserId); // 记录请求数据(同步日志) $txSeq = CcbMD5::generateTransactionSeq(); @@ -355,10 +305,9 @@ class CcbOrderService * @param array $order 订单数组 * @param array $orderItems 订单商品列表 * @param string $ccbUserId 建行用户ID - * @param string $payFlowId 支付流水号(从shopro_pay表获取) * @return array */ - private function buildOrderData($order, $orderItems, $ccbUserId, $payFlowId) + private function buildOrderData($order, $orderItems, $ccbUserId) { // 构建SKU商品列表(JSON字符串格式) $skuList = $this->buildSkuList($orderItems); @@ -382,7 +331,7 @@ class CcbOrderService 'REFUND_STATUS' => $this->mapRefundStatus($order['refund_status'] ?? 0), // 退款状态 'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称 'CUS_ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接 - 'PAY_FLOW_ID' => $payFlowId, // 支付流水号(必填!) + 'PAY_FLOW_ID' => $order['ccb_pay_flow_id'], // 支付流水号(必填!) 'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!) 'SKU_LIST' => $skuList, // 商品信息JSON字符串(必填!) // ========== 非必填字段 ========== @@ -425,7 +374,7 @@ class CcbOrderService // 返回JSON字符串(不转义Unicode,保持中文可读) return json_encode($skuList, JSON_UNESCAPED_UNICODE); } - + /** * 记录同步日志 * diff --git a/addons/shopro/library/ccblife/CcbPaymentService.php b/addons/shopro/library/ccblife/CcbPaymentService.php index c0cc2a9..92b6b7f 100644 --- a/addons/shopro/library/ccblife/CcbPaymentService.php +++ b/addons/shopro/library/ccblife/CcbPaymentService.php @@ -35,56 +35,13 @@ class CcbPaymentService throw new \Exception('建行生活配置文件不存在'); } - // 处理BASE64格式的密钥,添加PEM包装 - $this->config = $this->processPemKeys($this->config); + // ✅ 修复: 删除processPemKeys()调用 + // 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误 + // CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式 $this->orderService = new CcbOrderService(); } - /** - * 处理PEM格式密钥 - * 如果密钥是BASE64格式(不含-----BEGIN-----),则添加PEM包装 - * - * @param array $config 配置数组 - * @return array - */ - private function processPemKeys($config) - { - // 处理私钥 - if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) { - $config['private_key'] = "-----BEGIN PRIVATE KEY-----\n" - . chunk_split($config['private_key'], 64, "\n") - . "-----END PRIVATE KEY-----"; - } - - // 处理公钥 - if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) { - $config['public_key'] = "-----BEGIN PUBLIC KEY-----\n" - . chunk_split($config['public_key'], 64, "\n") - . "-----END PUBLIC KEY-----"; - } - - // 兼容merchant_public_key字段 - if (empty($config['merchant_public_key'])) { - $config['merchant_public_key'] = $config['public_key']; - } - - // 处理平台公钥 - if (!empty($config['platform_public_key'])) { - // 如果有配置平台公钥且是BASE64格式,添加PEM包装 - if (strpos($config['platform_public_key'], '-----BEGIN') === false) { - $config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n" - . chunk_split($config['platform_public_key'], 64, "\n") - . "-----END PUBLIC KEY-----"; - } - } else { - // 如果没有配置平台公钥,使用商户公钥作为默认值 - $config['platform_public_key'] = $config['public_key']; - } - - return $config; - } - /** * 生成建行支付串 * 用于前端JSBridge调用建行收银台 @@ -155,9 +112,9 @@ class CcbPaymentService $platformPubKey = $this->config['public_key']; // 服务方公钥 $mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); - // 使用RSA加密商户公钥后30位(用于ENCPUB字段) - $encryption = new CcbEncryption($this->config); - $encpub = $encryption->encryptMerchantPublicKeyLast30(); + // ✅ 修复:使用 CcbRSA 加密商户公钥后30位(用于ENCPUB字段) + // 删除 CcbEncryption 类,统一使用 CcbRSA 处理密钥格式化 + $encpub = $this->encryptPublicKeyLast30(); // 组装最终支付串 $finalPaymentString = $signString . '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub); @@ -247,6 +204,43 @@ class CcbPaymentService return implode(',', $orderItems); } + /** + * 加密商户公钥后30位(用于支付串的ENCPUB字段) + * + * 根据建行文档v2.2规范: + * "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文" + * + * @return string BASE64编码的加密密文 + * @throws \Exception + */ + private function encryptPublicKeyLast30() + { + $publicKey = $this->config['public_key'] ?? ''; + + if (empty($publicKey)) { + throw new \Exception('服务方公钥未配置'); + } + + // 去除 PEM 格式的头尾和空白字符,获取纯 BASE64 内容 + $publicKeyContent = str_replace([ + '-----BEGIN PUBLIC KEY-----', + '-----END PUBLIC KEY-----', + '-----BEGIN RSA PUBLIC KEY-----', + '-----END RSA PUBLIC KEY-----', + "\r", "\n", " ", "\t" + ], '', $publicKey); + + // 取后30位 + $last30Chars = substr($publicKeyContent, -30); + + if (strlen($last30Chars) < 30) { + throw new \Exception('商户公钥长度不足30位'); + } + + // ✅ 使用 CcbRSA 类进行加密(统一密钥格式化逻辑) + return CcbRSA::encrypt($last30Chars, $publicKey); + } + /** * 处理支付回调 * 建行支付完成后的同步回调 diff --git a/addons/shopro/library/ccblife/CcbRSA.php b/addons/shopro/library/ccblife/CcbRSA.php index 402e3f4..69437a2 100644 --- a/addons/shopro/library/ccblife/CcbRSA.php +++ b/addons/shopro/library/ccblife/CcbRSA.php @@ -140,9 +140,10 @@ class CcbRSA return $publicKey; } - // 格式化为PEM格式 + // ✅ 修复: chunk_split()会在末尾添加换行符,需要用rtrim()去除 + // 否则会导致PEM格式中密钥内容和尾部之间有多余空行,OpenSSL解析失败 $pem = "-----BEGIN PUBLIC KEY-----\n"; - $pem .= chunk_split($publicKey, 64, "\n"); + $pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n"; $pem .= "-----END PUBLIC KEY-----\n"; return $pem; @@ -166,9 +167,10 @@ class CcbRSA return $privateKey; } - // 格式化为PEM格式 + // ✅ 修复: chunk_split()会在末尾添加换行符,需要用rtrim()去除 + // 否则会导致PEM格式中密钥内容和尾部之间有多余空行,OpenSSL解析失败 $pem = "-----BEGIN RSA PRIVATE KEY-----\n"; - $pem .= chunk_split($privateKey, 64, "\n"); + $pem .= rtrim(chunk_split($privateKey, 64, "\n"), "\n") . "\n"; $pem .= "-----END RSA PRIVATE KEY-----\n"; return $pem;