This commit is contained in:
Billy 2025-10-23 17:00:15 +08:00
parent e86985e733
commit f917f62010
2 changed files with 99 additions and 31 deletions

View File

@ -54,16 +54,27 @@ return [
'private_key' => $envVars['private_key'] ?? '', 'private_key' => $envVars['private_key'] ?? '',
/** /**
* 商户公钥 (商户自己生成的RSA公钥,对应上面的私) * 服务方公钥 (服务方自己生成的RSA公)
* 用途: * 用途:
* - 提交给建行用于验证商户签名
* - 支付下单的MD5签名计算(PLATFORMPUB字段) * - 支付下单的MD5签名计算(PLATFORMPUB字段)
* - 加密商户公钥(ENCPUB字段) * - 用于加密商户公钥生成ENCPUB
* 格式: BASE64格式(不含PEM头尾) PEM格式(含头尾)
*
* 📌 注意: 这是参与支付签名计算的服务方公钥
*/
'public_key' => $envVars['public_key'] ?? '',
/**
* 商户公钥 (商户自己生成的RSA公钥,与上面的private_key对应)
* 用途:
* - 生成ENCPUB密文(使用服务方公钥加密商户公钥后30位)
* - 提交给建行用于验证商户签名
* 格式: BASE64格式(不含PEM头尾) PEM格式(含头尾) * 格式: BASE64格式(不含PEM头尾) PEM格式(含头尾)
* *
* 📌 注意: 需要将此公钥提交给建行进行配置 * 📌 注意: 需要将此公钥提交给建行进行配置
* 📌 如果已上架建行生活并同步公钥可以不再上送ENCPUB
*/ */
'public_key' => $envVars['public_key'] ?? '', 'merchant_public_key' => $envVars['merchant_public_key'] ?? ($envVars['public_key'] ?? ''),
/** /**
* 建行平台API公钥 (建行生活平台提供的RSA公钥) * 建行平台API公钥 (建行生活平台提供的RSA公钥)

View File

@ -119,6 +119,16 @@ class CcbPaymentService
$macParams['PROINFO'] = $proinfo; $macParams['PROINFO'] = $proinfo;
$macParams['REFERER'] = ''; // 商户URL空字符串 $macParams['REFERER'] = ''; // 商户URL空字符串
// ⚠️ 关键修复INSTALLNUM必须在THIRDAPPINFO之前严格按照文档4.1表格顺序)
// 1.3 可选参数按文档表格顺序有值才参与MAC
// 注意根据文档4.2橙色字段有值时才参与MAC空值不参与
// 分期期数在REFERER之后THIRDAPPINFO之前
if (!empty($this->config['install_num'])) {
$macParams['INSTALLNUM'] = $this->config['install_num'];
}
$macParams['THIRDAPPINFO'] = 'comccbpay1234567890cloudmerchant'; // 客户端标识(固定值) $macParams['THIRDAPPINFO'] = 'comccbpay1234567890cloudmerchant'; // 客户端标识(固定值)
// 记录关键参数 // 记录关键参数
@ -126,14 +136,6 @@ class CcbPaymentService
Log::info('[建行支付] 商户信息 merchant_id:' . ($macParams['MERCHANTID'] ?? 'N/A') . ' pos_id:' . ($macParams['POSID'] ?? 'N/A') . ' branch_id:' . ($macParams['BRANCHID'] ?? 'N/A')); Log::info('[建行支付] 商户信息 merchant_id:' . ($macParams['MERCHANTID'] ?? 'N/A') . ' pos_id:' . ($macParams['POSID'] ?? 'N/A') . ' branch_id:' . ($macParams['BRANCHID'] ?? 'N/A'));
Log::info('[建行支付] 商品信息 proinfo:' . $proinfo); Log::info('[建行支付] 商品信息 proinfo:' . $proinfo);
// 1.3 可选参数按文档表格顺序有值才参与MAC
// ⚠️ 注意根据文档4.2橙色字段有值时才参与MAC空值不参与
// 分期期数在THIRDAPPINFO之后
if (!empty($this->config['install_num'])) {
$macParams['INSTALLNUM'] = $this->config['install_num'];
}
// 超时时间 // 超时时间
if (!empty($this->config['timeout'])) { if (!empty($this->config['timeout'])) {
$macParams['TIMEOUT'] = $this->config['timeout']; $macParams['TIMEOUT'] = $this->config['timeout'];
@ -413,40 +415,95 @@ class CcbPaymentService
} }
/** /**
* 加密商户公钥后30位用于支付串的ENCPUB字段 * 生成ENCPUB商户公钥密文
* *
* 根据建行文档v2.2规范: * 根据建行文档4.4
* "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文" * "使用服务方公钥对商户公钥后30位进行RSA加密再进行base64后生成的密文串"
*
* 注意:这里的"商户公钥后30位"是指RSA公钥模数(modulus)的十六进制表示的后30位
* 不是BASE64字符串的后30位
* *
* @return string BASE64编码的加密密文 * @return string BASE64编码的加密密文
* @throws \Exception * @throws \Exception
*/ */
private function encryptPublicKeyLast30() private function encryptPublicKeyLast30()
{ {
$publicKey = $this->config['public_key'] ?? ''; // 获取商户公钥(被加密的公钥)
$merchantPublicKey = $this->config['merchant_public_key'] ?? '';
if (empty($publicKey)) { // 获取服务方公钥(用于加密的公钥)
$servicePublicKey = $this->config['public_key'] ?? '';
if (empty($merchantPublicKey)) {
throw new \Exception('商户公钥未配置');
}
if (empty($servicePublicKey)) {
throw new \Exception('服务方公钥未配置'); throw new \Exception('服务方公钥未配置');
} }
// 去除 PEM 格式的头尾和空白字符,获取纯 BASE64 内容 // ✅ 关键修复提取商户公钥模数的十六进制表示的后30位
$publicKeyContent = str_replace([ try {
'-----BEGIN PUBLIC KEY-----', // 格式化商户公钥为PEM格式
'-----END PUBLIC KEY-----', $merchantPemKey = $this->formatPublicKeyToPem($merchantPublicKey);
'-----BEGIN RSA PUBLIC KEY-----',
'-----END RSA PUBLIC KEY-----',
"\r", "\n", " ", "\t"
], '', $publicKey);
// 取后30位 // 解析商户公钥提取模数modulus
$last30Chars = substr($publicKeyContent, -30); $keyResource = openssl_pkey_get_public($merchantPemKey);
if (!$keyResource) {
if (strlen($last30Chars) < 30) { throw new \Exception('商户公钥格式错误: ' . openssl_error_string());
throw new \Exception('商户公钥长度不足30位');
} }
// ✅ 使用 CcbRSA 类进行加密(统一密钥格式化逻辑) $keyDetails = openssl_pkey_get_details($keyResource);
return CcbRSA::encrypt($last30Chars, $publicKey); openssl_free_key($keyResource);
if (!isset($keyDetails['rsa']['n'])) {
throw new \Exception('无法提取商户公钥RSA模数');
}
// 将模数转换为十六进制字符串去掉前导0x
$modulusHex = bin2hex($keyDetails['rsa']['n']);
// 取后30位十六进制字符
$last30Chars = substr($modulusHex, -30);
if (strlen($last30Chars) < 30) {
throw new \Exception('商户公钥模数长度不足30位');
}
Log::info('[建行支付] 商户公钥模数后30位: ' . $last30Chars);
// 使用服务方公钥加密这30位十六进制字符串
// CcbRSA::encrypt 会自动进行base64编码
return CcbRSA::encrypt($last30Chars, $servicePublicKey);
} catch (\Exception $e) {
Log::error('[建行支付] ENCPUB生成失败: ' . $e->getMessage());
throw $e;
}
}
/**
* 格式化公钥为PEM格式
*
* @param string $publicKey BASE64或PEM格式的公钥
* @return string PEM格式的公钥
*/
private function formatPublicKeyToPem($publicKey)
{
// 移除可能存在的空格和换行
$publicKey = preg_replace('/\s+/', '', $publicKey);
// 如果已经是PEM格式直接返回
if (strpos($publicKey, '-----BEGIN PUBLIC KEY-----') !== false) {
return $publicKey;
}
// 转换为PEM格式
$pem = "-----BEGIN PUBLIC KEY-----\n";
$pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n";
$pem .= "-----END PUBLIC KEY-----\n";
return $pem;
} }
/** /**