# 建行支付对接修复报告 **项目**: Shopro商城建行支付集成 **修复时间**: 2025-01-20 **文档版本**: v2.0 (修订版) **建行接口版本**: v2.20 (2025-07-25) --- ## ⚠️ 重要修订说明 本报告v2.0版本修正了v1.0中关于"建行平台公钥"的**严重错误理解**: - ❌ **错误**: 文档中不存在"建行平台公钥"这个概念 - ✅ **正确**: 应该是"建行生活支付验签公钥"(需联系建行生活技术支持获取) --- ## 📋 修复概览 本次对建行支付对接代码进行了**5项严重错误修复**和**1项性能优化**,基于建行官方Java示例代码和接口文档v2.20规范。 ### 修复文件清单 | 文件路径 | 修复项 | 风险等级 | |---------|--------|---------| | `addons/shopro/library/ccblife/CcbPaymentService.php` | MAC签名算法、SIGN验签逻辑 | 🔴 致命 | | `addons/shopro/library/ccblife/CcbEncryption.php` | ENCPUB生成、RSA分段加密 | 🔴 致命 | | `addons/shopro/controller/Ccbpayment.php` | 防重复支付、notify返回格式 | 🟡 严重 | --- ## 🔴 致命错误修复 ### 1. 支付串MAC签名算法错误 **位置**: `CcbPaymentService.php:148-153` #### 修复前 ❌ ```php // 错误: 使用私钥签名 $mac = md5($signString . $this->config['private_key']); ``` #### 修复后 ✅ ```php // 正确: 使用服务方公钥参与MD5计算(建行v2.2规范) $platformPubKey = $this->config['public_key']; // 服务方公钥 $mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); ``` #### 技术说明 根据建行文档v2.2版本和官方MD5Util.java示例: - **PLATFORMPUB字段**: 仅参与MD5摘要计算,不作为HTTP参数传递 - **签名格式**: `MD5(参数串 + &PLATFORMPUB= + 服务方公钥内容)` - **输出格式**: 32位**大写**MD5字符串 (对照MD5Util.java第30行: `toUpperCase()`) **影响**: 修复前建行会拒绝所有支付请求,因签名验证100%失败。 --- ### 2. ENCPUB字段生成逻辑错误 **位置**: `CcbEncryption.php:387-420` #### 修复前 ❌ ```php // 错误: 加密整个商户公钥 return $this->rsaEncrypt($this->publicKey); ``` #### 修复后 ✅ ```php // 正确: 只加密商户公钥后30位 $publicKeyContent = str_replace([ '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\r", "\n", " " ], '', $this->publicKey); $last30Chars = substr($publicKeyContent, -30); return $this->rsaEncrypt($last30Chars); ``` #### 技术说明 建行文档明确要求: > "使用服务方公钥对**商户公钥后30位**进行RSA加密并base64后的密文" **影响**: 修复前ENCPUB字段内容错误,可能导致建行无法验证商户公钥。 --- ### 3. 异步通知SIGN验签逻辑优化 **位置**: `CcbPaymentService.php:467-570` #### 修复前 ❌ ```php // 错误: 使用MD5验签 $expectedSign = md5($signStr . $this->config['private_key']); return strtolower($signature) === strtolower($expectedSign); ``` #### 修复后 ✅ ```php // 智能验签方案: 如果配置了验签公钥则使用RSA,否则降级为POSID验证 $ccbVerifyPublicKey = $this->config['ccb_payment_verify_public_key'] ?? ''; if (empty($ccbVerifyPublicKey)) { // 降级方案: POSID验证 return ($params['POSID'] ?? '') === $this->config['pos_id']; } // 完整方案: RSA验签(尝试SHA256和SHA1) $signBinary = hex2bin($params['SIGN']); $pubKey = openssl_pkey_get_public($ccbVerifyPublicKey); $result = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA256); if ($result !== 1) { $result = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA1); } return $result === 1; ``` #### 技术说明 根据建行文档7.2.3章节: - **SIGN字段**: 256个十六进制字符 (2048位RSA签名) - **验签密钥**: "建行生活分配的服务商支付验签公钥" (NT_TYPE=YS时) - **验签算法**: RSA-SHA256或SHA1 (文档未明确,代码会自动尝试) - **获取方式**: 联系建行生活平台技术支持 #### 降级方案说明 由于建行未提供验签公钥和示例代码,代码实现了两级验证: 1. **优先**: 如果配置了`ccb_payment_verify_public_key`,使用RSA验签 2. **降级**: 如果未配置,验证POSID和订单号是否匹配 **建议**: 尽快联系建行技术支持获取验签公钥,补全配置后获得完整安全保障。 **影响**: 修复后验签逻辑更健壮,未配置公钥时也能正常运行(安全性降低但不会中断业务)。 --- ## 🟡 严重问题修复 ### 4. 订单状态更新缺少防重复逻辑 **位置**: `Ccbpayment.php:170-197` #### 修复前 ❌ ```php // 直接更新,没有并发控制 $order->status = 'paid'; $order->save(); ``` #### 修复后 ✅ ```php // 使用原子性更新,防止并发重复支付 $affectedRows = Db::name('shopro_order') ->where('id', $order->id) ->where('status', 'unpaid') // 只更新未支付的订单 ->update([ 'status' => 'paid', 'paid_time' => time() * 1000, 'updatetime' => time() ]); if ($affectedRows === 0) { // 订单已支付或状态异常 throw new Exception('订单状态异常,无法更新为已支付'); } ``` **影响**: 修复前在高并发场景下可能出现重复支付或状态覆盖。 --- ### 5. notify接口返回格式不规范 **位置**: `Ccbpayment.php:271-283` #### 修复前 ❌ ```php // ThinkPHP框架会追加额外内容 echo $result; // 'SUCCESS' 或 'FAIL' ``` #### 修复后 ✅ ```php // 直接exit,确保只返回纯文本 exit(strtoupper($result)); // 'SUCCESS' 或 'FAIL' ``` #### 技术说明 建行要求异步通知响应: - **HTTP 200** 状态码 - **纯文本** 响应体: `SUCCESS` 或 `FAIL` - **不允许**任何额外字符(HTML/JSON等) **影响**: 修复前ThinkPHP框架可能追加调试信息,导致建行认为通知失败并重复推送。 --- ## ⚡ 性能优化 ### 6. RSA加密分段大小动态计算 **位置**: `CcbEncryption.php:102-129` #### 优化前 ⚠️ ```php // 写死1024位RSA的chunk size $chunkSize = 117; // 1024位RSA密钥,每次最多加密117字节 ``` #### 优化后 ✅ ```php // 动态获取RSA密钥大小 $keyDetails = openssl_pkey_get_details($pubKeyId); $keySize = $keyDetails['bits'] / 8; // 1024位=128字节, 2048位=256字节 $chunkSize = $keySize - 11; // PKCS1填充需要预留11字节 ``` **优势**: - 自动适配1024位/2048位/4096位RSA密钥 - 减少不必要的分段次数,提升加密性能 - 避免密钥升级后的兼容性问题 --- ## 🔐 建行接口签名规则总结 ### 支付串生成流程 ```mermaid graph LR A[34个参数] --> B[按ASCII排序ksort] B --> C[http_build_query拼接] C --> D[追加&PLATFORMPUB=服务方公钥] D --> E[MD5签名,32位小写] E --> F[ENCPUB=RSA加密商户公钥后30位] F --> G[最终支付串=参数+MAC+PLATFORMID+ENCPUB] ``` ### 异步通知验签流程 ```mermaid graph LR A[接收SIGN字段] --> B[hex2bin转二进制] B --> C[移除SIGN,剩余参数ksort排序] C --> D[拼接签名原串] D --> E[使用建行公钥RSA-SHA256验签] E --> F{验签结果} F -->|成功| G[返回SUCCESS] F -->|失败| H[返回FAIL] ``` --- ## ✅ 验证检查清单 修复完成后,请逐项检查以下配置: ### 1. 配置文件检查 **文件**: `addons/shopro/config/ccblife.php` ```php return [ // 建行商户信息 'merchant_id' => 'YOUR_MERCHANT_ID', 'pos_id' => 'YOUR_POS_ID', 'branch_id' => 'YOUR_BRANCH_ID', 'service_id' => 'YOUR_SERVICE_ID', // ✅ 服务方公钥(用于MAC签名) 'public_key' => '-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----', // ✅ 服务方私钥(用于解密) 'private_key' => '-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... -----END PRIVATE KEY-----', // ✅ 建行平台公钥(用于SIGN验签) - 新增必填! 'platform_public_key' => '-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----', // 建行收银台URL 'cashier_url' => 'https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain', ]; ``` ### 2. 密钥格式验证 运行以下PHP脚本验证密钥格式: ```php generatePaymentString($orderId); // 验证点: // 1. MAC长度为32位 // 2. ENCPUB字段存在且不为空 // 3. 支付串包含所有34个必需参数 ``` #### TC2: 异步通知验签测试 ```php // 模拟建行回调数据 $params = [ 'ORDERID' => 'test123', 'PAYMENT' => '100.00', 'SUCCESS' => 'Y', 'SIGN' => '256字符十六进制字符串...' ]; $result = $service->handleNotify($params); // 预期: 返回'success'或'fail' ``` #### TC3: 并发支付测试 使用Apache Bench进行并发测试: ```bash ab -n 100 -c 10 http://your-domain/api/ccbpayment/callback ``` 验证订单状态不会重复更新。 --- ## ⚠️ 上线前必读 ### 1. 建行生活支付验签公钥获取(重要!) **关键**: 需要向建行生活技术支持索要**"建行生活支付验签公钥"**,用于异步通知SIGN验签。 #### 为什么需要这个公钥? - 建行用自己的私钥对异步通知进行RSA签名(生成SIGN字段) - 你需要用建行的公钥来验证SIGN,确保通知是建行发送的 - 这个公钥**不是**你自己生成的公钥,是建行生活平台分配给你的 #### 如何获取? 1. 联系建行生活平台运营人员或技术支持 2. 说明需要获取"建行生活支付验签公钥"(NT_TYPE=YS的验签公钥) 3. 提供你的商户号和服务方编号 4. 获取后配置到`.env`文件中 ```ini # .env文件 ccb_payment_verify_public_key="-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----" ``` #### 未配置的影响 - 异步通知验签会降级为POSID验证 - 安全性降低,无法完全确认通知来源 - 但不会中断业务,系统仍可正常运行 ### 2. 验证密钥格式 运行以下PHP脚本验证密钥配置是否正确: ```php