php 银联支付-app

申请账号官网 https://open.unionpay.com/tjweb/index
开放平台    https://open.unionpay.com/tjweb/support/faq/mchlist?id=21


<?php

namespace Foreend\ThirdParty\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Log;

const COMPANY = "中国银联股份有限公司";
// 银联对接 接口
class UnionPayController extends Controller
{

    private static $config_merId = '777290058184064' ;     //商户代码,已被批准加入银联互联网系统的商户代码
    private static $pfx_pwd = '000000' ; // 签名证书密码,测试环境固定000000,生产环境请修改为从 cfca 下载的正式证书的密码,正式环境证书密码位数需小于等于6位,否则上传到商户服务网站会失败
    private static $pay_environment = 'test' ; // 测试环境  test  正式环境 formal



    private static $config = [] ;
    private static $pfx_url = '' ;
    private static $enc_url = '' ;
    private static $middle_url = '' ;
    private static $root_url = '' ;


    private static $pay_environment_url = '' ; // 当前接口 请求的地址
    private static $url = [ // 测试 支付网址
        'test'=>[
            'appTransUrl'=>'https://gateway.test.95516.com/gateway/api/appTransReq.do', // 支付接口 获取 tn
            'singleQueryUrl'=>'https://gateway.test.95516.com/gateway/api/queryTrans.do', // 查询接口 获取支付信息
        ],
        'formal'=>[
            'appTransUrl'=>'https://gateway.95516.com/gateway/api/appTransReq.do', // 支付接口 获取 tn
            'singleQueryUrl'=>'https://gateway.95516.com/gateway/api/queryTrans.do', // 查询接口 获取支付信息
        ]

    ] ;
    private static $verifyCerts510 = [];
    private static $ifValidateCNName = false; // 是否验证验签证书的CN,测试环境请设置false,生产环境请设置true。非false的值默认都当true处理。
    private static $ifValidateRemoteCert = false; // 是否验证https证书,测试环境请设置false,生产环境建议优先尝试true,不行再false。非true的值默认都当false处理。

    private static $config_version = '5.1.0' ;            //版本号
    private static $config_encoding = 'utf-8' ;          //编码方式
    private static $config_signMethod = '01' ;         //签名方法
    private static $config_txnType = '01' ;           // 交易类型
    /** $config_txnType
    //  00 查询交易 01:消费 02:预授权 03:预授权完成 04:退货 05: 圈存 11:代收  12:代付  13:账单支付  14:转账(保留)  21:批量交易  22:批量查询
    //  31:消费撤销  32:预授权撤销  33:预授权完成撤销  71:余额查询  72:实名认证-建立绑定关系  73: 账单查询
    //  74:解除绑定关系  75:查询绑定关系  77:发送短信验证码交易  78:开通查询交易  79:开通交易  94:IC 卡脚本通知
    */
    private static $config_txnSubType = '01' ;         //交易子类  依据实际交易类型填写  默认取值:01
    private static $config_bizType = '000201' ;       //产品类型
    /** $config_bizType
    依据实际业务场景填写(目前仅使用后 4 位,签名 2 位
    默认为 00)
    默认取值:000000
    具体取值范围:
    000101 基金业务之股票基金
    000102 基金业务之货币基金
    000201 B2C 网关支付
    000301 认证支付 2.0
    000302 评级支付
    000401 代付
    000501 代收
    000601 账单支付
    000801 跨行收单
    000901 绑定支付
    001001 订购
    000202 B2B
     */

    /**
     * @param $data
     * @return array
     */
    public static function pay($data){
        self::$config = [
            'version' => self::$config_version,
            'encoding' => self::$config_encoding,
            'bizType' => self::$config_bizType,
            'txnTime' => date('YmdHis'),   //订单发送时间,格式为YYYYMMDDhhmmss,取北京时间,此处默认取demo演示页面传递的参数
            'backUrl' => url('api/union/notify_url'),    //后台通知地址
            'currencyCode' => '156',             //交易币种,境内商户固定156
            'txnAmt' => $data['pay_amount'] * 100, //交易金额,单位分,此处默认取demo演示页面传递的参数
            'txnType' => self::$config_txnType,
            'txnSubType' => self::$config_txnSubType,
            'accessType' => '0',                // 接入类型
//            'signature'=>'', // 后面获取
            'signMethod' => self::$config_signMethod, // 01(表示采用RSA签名) HASH表示散列算法
            'channelType' => '08',               //渠道类型,07-PC,08-手机
            'merId' => self::$config_merId,
            'orderId' => $data['numbers'], //商户订单号,8-32位数字字母,不能含“-”或“_”,此处默认取demo演示页面传递的参数,可以自行定制规则
            'orderDesc' => $data['shop_name'], // 订单名称 这个好像没啥用
//            'frontUrl' => url('api/union/return_url'),  //前台通知地址

            //TODO 以下信息需要填写

            // 请求方保留域,
            // 透传字段,查询、通知、对账文件中均会原样出现,如有需要请启用并修改自己希望透传的数据。
            // 出现部分特殊字符时可能影响解析,请按下面建议的方式填写:
            // 1. 如果能确定内容不会出现&={}[]"'等符号时,可以直接填写数据,建议的方法如下。
            //    'reqReserved' =>'透传信息1|透传信息2|透传信息3',
            // 2. 内容可能出现&={}[]"'符号时:
            // 1) 如果需要对账文件里能显示,可将字符替换成全角&={}【】“‘字符(自己写代码,此处不演示);
            // 2) 如果对账文件没有显示要求,可做一下base64(如下)。
            //    注意控制数据长度,实际传输的数据长度不能超过1024位。
            //    查询、通知等接口解析时使用base64_decode解base64后再对数据做后续解析。
            //    'reqReserved' => base64_encode('任意格式的信息都可以'),

            //TODO 其他特殊用法请查看 pages/api_05_app/special_use_purchase.php
        ] ;

        // 证书存放路径
        self::$pfx_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_sign.pfx' ;
        self::$enc_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_enc.cer' ;
        self::$middle_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_middle.cer' ;
        self::$root_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_root.cer' ;

        self::$config['orderDesc'] = $data['shop_name'] ;    // 订单名称
        self::$config['orderId'] = $data['numbers'] ;        // 订单号
        self::$config['txnAmt'] = (int)$data['pay_amount'] * 100 ;// 订单金额
        self::$config['txnTime'] = date('YmdHis');    // 交易时间 年月日时分秒
        // 获取签名
        $sign = self::pay_sign() ;
        if($sign['code'] != 200) return $sign ;
        self::$pay_environment_url = 'appTransUrl' ; // 重定义 请求接口域名
        $result_arr = self::pay_post() ;
        if(count($result_arr)<=0) return ['code'=>400,'msg'=>'请求无回应'] ;// 没收到200应答的情况
        self::$config = $result_arr ; // 重新赋值
        $pay_state = self::pay_validate() ;  // 验证应答是否成功
        if(!$pay_state)  return ['code'=>400,'msg'=>'验签失败'] ;
        return ['code'=>200,'msg'=>'成功','tn'=>$result_arr['tn']] ;
    }
    // 生成签名
    public static function pay_sign()
    {
        if(self::$config['signMethod']=='01')  {
            return self::signByCertInfo();
        } else {
            return ['code'=>400,'msg'=>'暂不支持'] ;
        }
    }
    public static function signByCertInfo()
    {
        // self::$config, self::$pfx_url, self::$pfx_pwd
        //证书ID
        $cert = self::get_cert_id() ;
        if($cert['code'] != 200) return $cert ;
        self::$config['certId'] = $cert['cert_id'];
        $private_key = $cert['ksy'];
        $params_str = self::createLinkString (true, false );  // 转换成key=val&串
        $params_sha256x16 = hash( 'sha256',$params_str);//sha256签名摘要
        $result = openssl_sign ( $params_sha256x16, $signature, $private_key, 'sha256');  // 签名
        if (!$result) return ['code'=>400,'msg'=>'>>>>>签名失败<<<<<<<']; ;
        self::$config ['signature'] = base64_encode ( $signature );
        return ['code'=>200,'msg'=>'成功'] ;
    }
    // 获取证书信息
    private static function get_cert_id()
    {
        $pkcs12certdata = file_get_contents ( self::$pfx_url );
        if($pkcs12certdata === false ) return ['code'=>400,'msg'=>'file_get_contents fail。'];
        if(openssl_pkcs12_read ( $pkcs12certdata, $certs, self::$pfx_pwd ) == FALSE ) return ['code'=>400,'msg'=>'openssl_pkcs12_read fail。'];
        $x509data = $certs ['cert'];
        if(!openssl_x509_read ( $x509data )) return  ['code'=>400,'msg'=>'openssl_x509_read fail。'];
        $certdata = openssl_x509_parse ( $x509data );
        $certId = $certdata ['serialNumber'];
        $certKey = $certs ['pkey'];
        $certCert = $x509data;
        return ['code'=>200,'msg'=>'ok','cert_id'=>$certId,'ksy'=>$certKey,'cert'=>$certCert] ;
    }
    /**
     * 讲数组转换为string
     * @param $sort  // 是否需要排序
     * @param $encode  // 是否需要URL编码
     * @return string
     */
    public static function createLinkString($sort, $encode) {
        $para = self::$config ;
        if ($sort) {
            $para = self::argSort ( $para );
        }
        $arr = [] ;
        $i = 0 ;
        foreach ($para as $k=>$v){
            $arr[$i] = $k.'='.($encode ? urlencode ( $v ): $v) ;
            $i++ ;
        }
        $linkString = implode('&',$arr) ;
        return $linkString;
    }
    /**
     * 对数组排序
     *
     * @param $para  // 排序前的数组
     *         return 排序后的数组
     */
    public static function argSort($para) {
        ksort ( $para );
        reset ( $para );
        return $para;
    }
    //  post 请求 支付
    public static function pay_post()
    {
        $opts = self::createLinkString ( false, true );
        $ch = curl_init ();
        curl_setopt ( $ch, CURLOPT_URL, self::$url[self::$pay_environment][self::$pay_environment_url] );
        curl_setopt ( $ch, CURLOPT_POST, 1 );
        curl_setopt ( $ch, CURLOPT_SSL_VERIFYPEER, false ); // 不验证证书
        curl_setopt ( $ch, CURLOPT_SSL_VERIFYHOST, false ); // 不验证HOST
        curl_setopt ( $ch, CURLOPT_SSLVERSION, 1 ); // http://php.net/manual/en/function.curl-setopt.php页面搜CURL_SSLVERSION_TLSv1
        curl_setopt ( $ch, CURLOPT_HTTPHEADER, array (
            'Content-type:application/x-www-form-urlencoded;charset=UTF-8'
        ) );
        curl_setopt ( $ch, CURLOPT_POSTFIELDS, $opts );
        curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
        $html = curl_exec ( $ch );
        if(curl_errno($ch)){
//            $errmsg = curl_error($ch);
            curl_close ( $ch );
            return null;
        }
        if( curl_getinfo($ch, CURLINFO_HTTP_CODE) != "200"){
//            $errmsg = "http状态=" . curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close ( $ch );
            return null;
        }
        curl_close ( $ch );
        $result_arr = self::parseQString ( $html );
        return $result_arr;
    }
    /**
     * 验签
     * @param $params // 应答数组 是否成功
     * @return
     */
    public static function pay_validate() {
        $params = self::$config ;
        $signature_str = $params ['signature'];
        unset ( $params ['signature'] );
        self::$config = $params ; // 重新定义数据
        $params_str = self::createLinkString (true, false );
        $strCert = $params['signPubKeyCert'];
        $strCert = self::verifyAndGetVerifyCert($strCert);
        if($strCert['code'] != 200) return $strCert ;
        $strCert = $strCert['msg'] ; // 证书内容
        $params_sha256x16 = hash('sha256', $params_str); // fcadf418d2c422912e5a823b60fb16af326e890f75f76ccdb5a80a465bbc2710
        $signature = base64_decode ( $signature_str );
        $isSuccess = openssl_verify ( $params_sha256x16, $signature,$strCert, "sha256" );
        return $isSuccess;
    }
    public static function verifyAndGetVerifyCert($certBase64String){
        if (array_key_exists($certBase64String, self::$verifyCerts510)){
            return ['code'=>200,'msg'=>self::$verifyCerts510[$certBase64String] ] ;
        }
        if (self::$middle_url === null || self::$root_url === null){
            return ['code'=>400,'msg'=>'rootCertPath or middleCertPath is none, exit initRootCert'] ;
        }
        openssl_x509_read($certBase64String);
        $certInfo = openssl_x509_parse($certBase64String);
        $cn = self::getIdentitiesFromCertficate($certInfo); // 中国银联股份有限公司
        if(strtolower(self::$ifValidateCNName) == "true"){
            if (COMPANY != $cn) return ['code'=>400,'msg'=>"cer owner is not CUP:" . $cn] ;
            if (COMPANY != $cn && "00040000:SIGN" != $cn) return ['code'=>400,'msg'=>"cer owner is not CUP:" . $cn] ;
        }
        $from = date_create ( '@' . $certInfo ['validFrom_time_t'] );
        $to = date_create ( '@' . $certInfo ['validTo_time_t'] );
        $now = date_create ( date ( 'Ymd' ) );
        $interval1 = $from->diff ( $now );
        $interval2 = $now->diff ( $to );
        if ($interval1->invert || $interval2->invert) return ['code'=>400,'msg'=>"signPubKeyCert has expired"] ;
        $result = openssl_x509_checkpurpose($certBase64String, X509_PURPOSE_ANY, [self::$root_url, self::$middle_url]);
        if($result === FALSE) return ['code'=>400,'msg'=>"validate signPubKeyCert by rootCert failed"] ;
         if($result === TRUE){
            self::$verifyCerts510[$certBase64String] = $certBase64String;
            return ['code'=>200,'msg'=>self::$verifyCerts510[$certBase64String] ] ;
        } else {
            return ['code'=>400,'msg'=>"validate signPubKeyCert by rootCert failed with error"] ;
        }
    }

    public static function getIdentitiesFromCertficate($certInfo){

        $cn = $certInfo['subject'];
        $cn = $cn['CN'];
        $company = explode('@',$cn);
        if(count($company) < 3) {
            return null;
        }
        return $company[2];
        /**
            0 => "041"
            1 => "8310000000083040"
            2 => "中国银联股份有限公司"
            3 => "00016495"
         */
    }

    /**
     * key1=value1&key2=value2转array
     * @param $str // key1=value1&key2=value2的字符串
     * @param $$needUrlDecode 是否需要解url编码,默认不需要
     */
    public static function parseQString($str, $needUrlDecode=false){
        $result = array();
        $len = strlen($str);
        $temp = "";
        $curChar = "";
        $key = "";
        $isKey = true;
        $isOpen = false;
        $openName = "\0";

        for($i=0; $i<$len; $i++){
            $curChar = $str[$i];
            if($isOpen){
                if( $curChar == $openName){
                    $isOpen = false;
                }
                $temp .= $curChar;
            } elseif ($curChar == "{"){
                $isOpen = true;
                $openName = "}";
                $temp .= $curChar;
            } elseif ($curChar == "["){
                $isOpen = true;
                $openName = "]";
                $temp .= $curChar;
            } elseif ($isKey && $curChar == "="){
                $key = $temp;
                $temp = "";
                $isKey = false;
            } elseif ( $curChar == "&" && !$isOpen){
                self::putKeyValueToDictionary($temp, $isKey, $key, $result, $needUrlDecode);
                $temp = "";
                $isKey = true;
            } else {
                $temp .= $curChar;
            }
        }
        self::putKeyValueToDictionary($temp, $isKey, $key, $result, $needUrlDecode);
        return $result;
    }


    public static function putKeyValueToDictionary($temp, $isKey, $key, &$result, $needUrlDecode) {
        if ($isKey) {
            $key = $temp;
            if (strlen ( $key ) == 0) {
                return false;
            }
            $result [$key] = "";
        } else {
            if (strlen ( $key ) == 0) {
                return false;
            }
            if ($needUrlDecode)
                $result [$key] = urldecode ( $temp );
            else
                $result [$key] = $temp;
        }
    }

    // 交易状态查询 返回的数据 和异步自己返回的数据差不多 除了业务类型不同都一样
    public static function union_notify_get_state($data)
    {
        self::$config  = [
                //以下信息非特殊情况不需要改动
                'version' => self::$config_version,         //版本号
                'encoding' => self::$config_encoding,       //编码方式
                'bizType' => '000802',      //业务类型
                'txnTime' => $data["txnTime"], //请修改被查询的交易的订单发送时间,格式为YYYYMMDDhhmmss,此处默认取demo演示页面传递的参数
                'txnType' => '00',          //交易类型
                'txnSubType' => '00',       //交易子类
                'accessType' => '0',        //接入类型
//                'signature'=>'' ,          // 签名后续文件补充
                'signMethod' => self::$config_signMethod,       //签名方法
                'merId' => self::$config_merId,        //商户代码,请改自己的测试商户号,此处默认取demo演示页面传递的参数
                'orderId' => $data["orderId"], //请修改被查询的交易的订单号,8-32位数字字母,不能含“-”或“_”,此处默认取demo演示页面传递的参数
//                'channelType' => '07',        //渠道类型
                //TODO 以下信息需要填写
            ];
        // 证书存放路径
        self::$pfx_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_sign.pfx' ;
        self::$enc_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_enc.cer' ;
        self::$middle_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_middle.cer' ;
        self::$root_url = getcwd().'/union_pay/'.self::$pay_environment.'/acp_test_root.cer' ;
        $sign = self::pay_sign() ;
        if($sign['code'] != 200) return $sign ;
        self::$pay_environment_url = 'singleQueryUrl' ; // 重定义 请求接口域名
        $result_arr = self::pay_post() ;
        if(count($result_arr)<=0) return ['code'=>400,'msg'=>'请求无回应'] ;// 没收到200应答的情况
        self::$config = $result_arr ; // 重新赋值
        $pay_state = self::pay_validate() ;  // 验证应答是否成功
        if(!$pay_state)  return ['code'=>400,'msg'=>'验签失败'] ;
        return ['code'=>200,'msg'=>'成功','tn'=>$result_arr] ;
    }
}

 

记录使用

 

acp_test_sign.pfx
acp_test_enc.cer
acp_test_middle.cer
acp_test_root.cer

四个测试文件 登录后获取

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值