建行互联网银企被扫支付

背景

最近在对接建行的支付,我们做的是被扫支付,就是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 

 

下面简单介绍下

签名计算流程

  1. 将所有的请求参数去掉空值,并按key升序排序
  2. 将第一步得到的数据,按key=value的形式进行拼接,用&隔开
  3. 将拼接后的字符串再拼接上"20120315201809041004"
  4. 将最后得到的字符串进行MD5加密,就是SIGN的值

加密串计算流程

  1. 把上面签名后的结果以键值对的形式放入请求参数中(所有的请求参数,含空值),键名是SIGN
  2. 将第一步得到的请求参数,按key=value的形式进行拼接,用&隔开,得到待加密的字符串
  3. 截取公钥的后30位,再截取这30位的前8位,得到一个8位的字符串,这个是参与加密串计算的公钥
  4. 先将第二步得到的待加密的字符串从"utf-8"编码转为"utf-16",并与第三步得到的8位的公钥用"DES-ECB"进行加密
  5. 把第四步得到的加密结果中的"+"替换为","
  6. 再对第五步的结果进行UrlEncode编码,得到的结果就是ccbParam

验签流程

  1. 建行接口所有返回的参数,只取接口文档中的"签名源文格式"中相关的数据,作为验签源数据
  2. 将返回的签名字段SIGN(十六进制),转为十进制
  3. 建行的公钥是DER格式的,且是十六进制,需要转为PEM格式。将完整的公钥转为十进制,同时进行base64编码,拼接上"-----BEGIN PUBLIC KEY-----"和"-----END PUBLIC KEY-----"做成pem
  4. 提取第三步得到的PEM证书的公钥
  5. 将第一步得到的验签源数据,按key=value的形式进行拼接,用&隔开,作为新的源数据
  6. 使用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": "" // 建行接口返回原文
}

退款麻烦麻烦在需要在建行商户平台配置一个操作员账号,此外还需要导出证书和配置,其他的基本上没了

 

posted @ 2020-12-11 18:07  凌雨尘  阅读(2393)  评论(21编辑  收藏  举报