zoukankan      html  css  js  c++  java
  • 建行互联网银企被扫支付

    背景

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

    下面简单介绍下

    签名计算流程

    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-----
    "
                . wordwrap(base64_encode(self::Hex2String($key)), 64, "
    ", true)
                . "
    -----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 包,开启一个服务就可以处理退款请求了

    启动服务,绑定的是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": "" // 建行接口返回原文
    }

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

  • 相关阅读:
    Java实现 LeetCode 32 最长有效括号
    Java实现 LeetCode 31下一个排列
    Java实现 LeetCode 31下一个排列
    Java实现 LeetCode 31下一个排列
    Java实现 蓝桥杯 素因子去重
    Java实现 蓝桥杯 素因子去重
    Java实现 蓝桥杯 素因子去重
    Java实现 LeetCode 30 串联所有单词的子串
    Visual c++例子,可不使用常规的对话框资源模板的情况下,动态创建对话框的方法
    MFC不使用对话框资源模版创建对话框
  • 原文地址:https://www.cnblogs.com/lyc94620/p/14121755.html
Copyright © 2011-2022 走看看