背景
最近在對接建行的支付,我們做的是被掃支付,就是B掃C,一開始對方發了一個壓縮包給我,看起來挺齊全的,文檔、demo啥的都有,以為很簡單,跟微信支付寶類似,調一下接口,驗證一下就OK了。然而,事實證明我還是太年輕了。而且網絡上你能夠搜到的基本上都用不了,所以記一下博客,或許可以幫助其他人。
先說一下建行支付比較特殊的地方吧
1、官方提供的demo里面,只有Java和.Net是有真正的demo,PHP和其他語言沒有,只提供一個dll文件,幾乎沒什么用
2、計算加密串的時候,待加密的數據要轉為十六進制
3、建行通知返回的SIGN是十六進制的,要轉為十進制
4、建行提供的公鑰是DER格式的,十六進制,而 MD5withRSA 進行加密驗證的時候,要轉成PEM格式
5、建行被掃支付文檔雖然說要用POST,但是實際上只能用GET
6、退款也是很惡心的一個東西,建行的退款走接口的話只能用外聯平台退款,支付接口里面退款的描述就幾句話
由於筆者是用PHP進行開發的,既然官方沒有提供PHP版的demo,只能根據Java版的翻譯成PHP版的。至於退款,只能開一個外聯平台服務進行處理了
支付
可參考筆者的 Github 項目,里面包含了完整的PHP加密驗簽方法,也包含了Java版的處理
PS:Github 如果網速比較慢,可以在這里下載
提取碼:4a7u
下面簡單介紹下
簽名計算流程
- 將所有的請求參數去掉空值,並按key升序排序
- 將第一步得到的數據,按key=value的形式進行拼接,用&隔開
- 將拼接后的字符串再拼接上"20120315201809041004"
- 將最后得到的字符串進行MD5加密,就是SIGN的值
加密串計算流程
- 把上面簽名后的結果以鍵值對的形式放入請求參數中(所有的請求參數,含空值),鍵名是SIGN
- 將第一步得到的請求參數,按key=value的形式進行拼接,用&隔開,得到待加密的字符串
- 截取公鑰的后30位,再截取這30位的前8位,得到一個8位的字符串,這個是參與加密串計算的公鑰
- 先將第二步得到的待加密的字符串從"utf-8"編碼轉為"utf-16",並與第三步得到的8位的公鑰用"DES-ECB"進行加密
- 把第四步得到的加密結果中的"+"替換為","
- 再對第五步的結果進行UrlEncode編碼,得到的結果就是ccbParam
驗簽流程
- 建行接口所有返回的參數,只取接口文檔中的"簽名源文格式"中相關的數據,作為驗簽源數據
- 將返回的簽名字段SIGN(十六進制),轉為十進制
- 建行的公鑰是DER格式的,且是十六進制,需要轉為PEM格式。將完整的公鑰轉為十進制,同時進行base64編碼,拼接上"-----BEGIN PUBLIC KEY-----"和"-----END PUBLIC KEY-----"做成pem
- 提取第三步得到的PEM證書的公鑰
- 將第一步得到的驗簽源數據,按key=value的形式進行拼接,用&隔開,作為新的源數據
- 使用MD5withRSA方法,將十進制的SIGN、源數據以及提取的公鑰進行驗證
代碼:
ccbPay.php
<?php require_once './ccbUtils.php'; /** * 被掃支付:建行互聯網銀企被掃支付(聚合) * Class ccbPay */ class ccbPay { // 商戶號 const MERCHANTID = '105910100190000'; // 櫃台號 const POSID = '000000000'; // 分行號 const BRANCHID = '610000000'; // 建行支付公鑰 const PUBKEY = '30819d300d06092a864886f70d010101050003818b0030818702818100a32fb2d51dda418f65ca456431bd2f4173e41a82bb75c2338a6f649f8e9216204838d42e2a028c79cee19144a72b5b46fe6a498367bf4143f959e4f73c9c4f499f68831f8663d6b946ae9fa31c74c9332bebf3cba1a98481533a37ffad944823bd46c305ec560648f1b6bcc64d54d32e213926b26cd10d342f2c61ff5ac2d78b020111'; // 請求接口域名 const HOST = 'https://ibsbjstar.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY'; /** * 建行支付,被掃 */ public function pay() { $data = [ 'MERCHANTID' => self::MERCHANTID, // 商戶號 'POSID' => self::POSID, // 櫃台號 'BRANCHID' => self::BRANCHID, // 分行號 'GROUPMCH' => '', // 集團商戶信息 'TXCODE' => 'PAY100', // 交易碼 'MERFLAG' => '', // 商戶類型 'TERMNO1' => '', // 終端編號 1 'TERMNO2' => '', // 終端編號 2 'ORDERID' => '', // 訂單號 'QRCODE' => '', // 碼信息(一維碼、二維碼) 'AMOUNT' => '0.01', // 訂單金額,單位:元 'PROINFO' => '', // 商品名稱 'REMARK1' => '', // 備注 1 'REMARK2' => '', // 備注 2 'FZINFO1' => '', // 分賬信息一 'FZINFO2' => '', // 分賬信息二 'SUB_APPID' => '', // 子商戶公眾賬號 ID 'RETURN_FIELD' => '', // 返回信息位圖 'USERPARAM' => '', // 實名支付 'detail' => '', // 商品詳情 'goods_tag' => '', // 訂單優惠標記 ]; $ccbUtils = new ccbUtils(); // 計算簽名 $sign = $ccbUtils->calSign($ccbUtils->sortParams($data)); $data['SIGN'] = $sign; // 計算加密串 $params = http_build_query($data); $pubKey = substr(self::PUBKEY, -30); $pubKey = substr($pubKey, 0, 8); $data['ccbParam'] = $ccbUtils->calCcbParam($params, $pubKey); // 獲取要請求的參數 $requestData = $ccbUtils->getRequestData($data); $url = self::HOST . '?' . http_build_query($requestData); var_dump($url); } /** * 支付查詢 */ public function query() { $data = [ 'MERCHANTID' => self::MERCHANTID, // 商戶號 'POSID' => self::POSID, // 櫃台號 'BRANCHID' => self::BRANCHID, // 分行號 'GROUPMCH' => '', // 集團商戶信息 'TXCODE' => 'PAY101', // 交易碼 'MERFLAG' => '', // 商戶類型 'TERMNO1' => '', // 終端編號 1 'TERMNO2' => '', // 終端編號 2 'ORDERID' => '', // 訂單號 'QRYTIME' => '', // 查詢次數 從1開始 'QRCODE' => '', // 碼信息(一維碼、二維碼) 'QRCODETYPE' => '', // 二維碼類型 如未上送 QRCODE 則此參數為必輸 'REMARK1' => '', // 備注 1 'REMARK2' => '', // 備注 2 'SUB_APPID' => '', // 子商戶公眾賬號 ID 'RETURN_FIELD' => '', // 返回信息位圖 ]; // 與支付的區別TXCODE不一樣,需要傳QRYTIME,QRCODE和QRCODETYPE兩個需傳一個 // 后續計算簽名和加密串跟支付類似 } public function refund() { // 退款只能走外聯平台 } /** * 建行返回參數sign驗簽 */ public function checkCcbSign() { // 建行返回的數據 $returnData = [ 'RESULT' => 'Y', 'ORDERID' => '151677281312212', 'AMOUNT' => '0.01', 'WAITTIME' => 'null', 'TRACEID' => '1010115031516772964428432', 'SIGN' => '80c3298a47b26cb9d8d708e1465c6b521edcce32b0deecab91257a3f41fc6cf39fa43afa54dc8489a04615eee9dcca1f4b52ce677f70109f29745ff34033018353b78e982cc860623b6c3df0d9c1a62ca010a019fff8544d4d8e154a010d7fc16cb590ccd87f34d8bea6added68cf1f9943fdb1d83616507a4588b68774b9fe1' ]; $ccbUtils = new ccbUtils(); $result = $ccbUtils->checkSign($ccbUtils->getCalSignData($returnData, ccbUtils::SIGN_CCB_PAY), self::PUBKEY); var_dump($result); } }
ccbUtils.php
<?php class ccbUtils { // 加密MD5 key const MD5KEY = '20120315201809041004'; // 驗證簽名用到的類型,1-支付接口,2-查詢接口 const SIGN_CCB_PAY = 1; const SIGN_CCB_QUERY = 2; /** * 按key升序排序,同時去掉空值 * @param $params array * @return mixed */ public function sortParams($params) { ksort($params); foreach ($params as $key => $value) { if (empty($value) && $value == '') { unset($params[$key]); } } return $params; } /** * 計算簽名 * @param $params array 不含空值 * @return string */ public function calSign($params) { return md5(http_build_query($params) . self::MD5KEY); } /** * 計算ccbparam * @param $params string * @param $key string * @return string */ public function calCcbParam($params, $key) { $res = openssl_encrypt (iconv("utf-8", "utf-16", $params), 'DES-ECB', $key); $res = str_replace('+', ',', $res); $res = urlencode($res); return $res; } /** * 真正請求建行接口要傳的參數 * @param $data array * @return array */ public function getRequestData($data) { return [ 'MERCHANTID' => $data['MERCHANTID'], 'POSID' => $data['POSID'], 'BRANCHID' => $data['BRANCHID'], 'ccbParam' => $data['ccbParam'], ]; } /** * 獲取要驗證簽名的參數 * @param $data array * @param $type int * @return array */ public function getCalSignData($data, $type) { switch ($type) { case self::SIGN_CCB_PAY: $res = [ 'RESULT' => $data['RESULT'], 'ORDERID' => $data['ORDERID'], 'AMOUNT' => $data['AMOUNT'], 'WAITTIME' => $data['WAITTIME'], 'TRACEID' => $data['TRACEID'], 'SIGN' => $data['SIGN'] ]; break; case self::SIGN_CCB_QUERY: $res = [ 'RESULT' => $data['RESULT'], 'ORDERID' => $data['ORDERID'], 'AMOUNT' => $data['AMOUNT'], 'WAITTIME' => $data['WAITTIME'], 'SIGN' => $data['SIGN'] ]; break; default: $res = []; break; } return $res; } /** * 驗證簽名 * @param $data array * @param $key string * @return bool */ public function checkSign($data, $key) { if (empty($data)) { return false; } $sign = $data['SIGN']; unset($data['SIGN']); $data = http_build_query($data); $pubkey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap(base64_encode(self::Hex2String($key)), 64, "\n", true) . "\n-----END PUBLIC KEY-----"; $pkeyId = openssl_pkey_get_public($pubkey); $verify = openssl_verify($data, self::Hex2String($sign), $pkeyId, OPENSSL_ALGO_MD5); openssl_free_key($pkeyId); return (bool) $verify; } /** * 十六進制轉字符串 * @param $hex string * @return string */ private function Hex2String($hex) { $string = ''; for ($i = 0; $i < strlen($hex) - 1; $i += 2) { $string .= chr(hexdec($hex[$i] . $hex[$i + 1])); } return $string; } /** * 字符串轉十六進制 * @param $str string * @return string */ private function String2Hex($str){ $hex=''; for ($i=0; $i < strlen($str); $i++){ $hex .= dechex(ord($str[$i])); } return $hex; } }
退款
建行退款只提供兩種方式
1、登錄商戶服務平台,手工處理退款
2、走外聯平台服務進行退款
官方給的文檔,教你搭建外聯平台都是基於Windows的,Linux的幾乎沒有,而且搭建流程非常復雜,而且你還得找一台服務器專門用來退款。Excuse me?
筆者提供一個 Github 項目,只用使用里面的 jar 包,開啟一個服務就可以處理退款請求了
退款Jar包源碼在這里:Github項目
啟動服務,綁定的是8080端口
# java -jar ccb-cloud-sdk-1.0-SNAPSHOT.jar
請求實例:
接口:http://127.0.0.1:8080/ccb/pay/refund 請求參數: { "merchantId": "商戶號", "custId": "操作員賬號", // 登錄建行商戶平台-服務管理-操作員管理,列表里面的客戶號 "transPwd": "操作員交易密碼", // 創建操作員時候填的 "certPassword": "證書密碼", // 導出證書的時候填的密碼 "txCode": "5W1004", // 參考"外聯平台商戶開發接口_V4.0.chm",退款是這個"5W1004" "language": "CN", "url": "https://merchant.ccb.com", "certFilePath": "/config/MC123456789.pfx", // 使用絕對路徑 "configFilePath": "/config/config.xml", // 使用絕對路徑 "refundNo": "序列號", // 16位以內純數字 "refundAmt": "退款金額", // 單位:元 "payRecordNo": "交易單號" // 交易的時候你傳給建行的單號 } 返回參數: { "return_CODE": "000000", // 參考"外聯平台商戶開發接口_V4.0.chm" "return_MSG": "退款成功", // 參考"外聯平台商戶開發接口_V4.0.chm" "order_NUM": "交易單號", // 交易的時候你傳給建行的單號 "tx_INFO": "" // 建行接口返回原文 }
退款麻煩麻煩在需要在建行商戶平台配置一個操作員賬號,此外還需要導出證書和配置,其他的基本上沒了