【PHP】微信支付apiV3平台代金券和商家券接口api,使用原生php处理

微信支付平台代金券和商家券接口api,使用原生php处理

当然,能使用composer加载的,直接使用composer加载微信官方包就完事儿了,别舍近求远得不偿失。

微信官方链接:

指导文档&SDK : https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml

相关V3-api : https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_1_10.shtml

 

1、 背景:最近公司使用比较老的php框架需要实现微信支付apiV3版本的代金券和商家券功能。

      本来微信官方有比较成熟的GuzzleHttp包实现composer加载开箱即用,可惜公司php框架太老了,不支持namespace和composer,没办法只能手撸

2、接口这块分2大块,一是普通接口api的签名、验签,加密和解密;二是图片上传接口的签名、验签、加密、解密;

3、参考过的链接,非常感谢:

  1)图片上传:https://blog.csdn.net/qq_16469571/article/details/105436553

  2)普通接口签名和验签:签名+加密 https://www.cnblogs.com/inkwhite/p/11686115.html

                                     验签+解密 https://blog.csdn.net/qq_27987023/article/details/88987835,

 

(tips:可以使用微信提供的postman的JSON文件示例包,测试相关证书、序列号是不是对的,链接https://github.com/wechatpay-apiv3/wechatpay-postman-script)

4、不废话,上代码:

设置回调验签+解密

    // 设置核销券通知地址
    public function setConsumeNotifyUrl( )
    {
        $url='https://api.mch.weixin.qq.com/v3/marketing/favor/callbacks';
        $notifyurl = C('wx_node_voucher.consume_notify');
        //echo $notifyurl,$url;
        $wxnodevoucher = D('WxNodeVoucher', 'Service');
        $data = [
            'mchid'=>$wxnodevoucher->mchId,
            'notify_url'=>$notifyurl,
        ];
        /*//生成V3请求 header认证信息
        $header = $wxnodevoucher->createAuthorization($url, 'POST', $data);
        //var_dump($header);
        $result = $wxnodevoucher->curlHttp( $url, 'POST', $data , $header );*/

        $result = $wxnodevoucher->send($url, 'POST', $data);
        exit(json_encode($result, JSON_UNESCAPED_UNICODE));
    }
    // 回调通知,需要验签
    public function notifyVoucher( )
    {
        $log = function($info){
            log_write("回调信息:{$info}", 'wx_voucher_notify');
        };
        $headers = $_SERVER;
        $log(json_encode($headers, JSON_UNESCAPED_UNICODE));

        $sign = $headers['Wechatpay-Signature'];
        $nonce = $headers['Wechatpay-Nonce'];
        $timestamp = $headers['Wechatpay-Timestamp'];
        $body = file_get_contents('php://input');

        $message = $timestamp."\n".
            $nonce."\n".
            $body."\n";

        $wxnodevoucher = D('WxNodeVoucher', 'Service');
        if( true === $wxnodevoucher->checkAuthorization($sign, $message) ){
            $log('回调通知验签通过');
            $data = json_decode($body, true);
            /**
             * ApiV3 key 执行AEAD_AES_256_GCM解密 -- https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_1_15.shtml
             */
            $remsg = $wxnodevoucher->decryptToString($wxnodevoucher->apiv3key ,$data['resource']['associated_data'], $data['resource']['nonce'], $data['resource']['ciphertext']);
            if(false === $remsg) {
                $log('解密失败,body=' . $body . ', apiv3key=' . $wxnodevoucher->apiv3key);
                // TODO 发送邮件
                return false;
            }
            $sourceArr = json_decode($remsg, 1);
            if (json_last_error() || !is_array($sourceArr)) {
                // TODO 解析json串异常
                $log('解析json串异常,,remsg=' . $remsg);
            }
            switch ($data['event_type']) {
                case 'COUPON.USE':
                    // TODO 卡券核销
                    $this->consumeVoucher($data, $sourceArr);
                    break;
                case '':
                default:
                    // TODO 证书下载
                    break;
            }
            exit(json_encode(['code'=>'SUCCESS', 'message'=>'成功']));
        }else{
            $log('回调通知验签失败');
        }
    }

发送普通api签名+加密

    public function initAutoload()
    {
        if (!$this->initAutoload) {
            // 商户配置
            $this->appid = C('wx_node_voucher.miniproject_appid');
            $this->mchId = C('wx_node_voucher.mch_id');
            $this->mch_serial_no = C('wx_node_voucher.mch_serial_no');
            $this->mch_private_key = C('wx_node_voucher.apiclient_key');
            $this->mch_cert_pem = C('wx_node_voucher.apiclient_cert');
            $this->apiv3key = C('wx_node_voucher.apiv3_key');
            $this->platform_cert_pem = C('wx_node_voucher.weixin_cert');
        }
    }

    //生成v3 Authorization
    public function createAuthorization( $url , $method = 'GET', array $data=[] ){

        if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
            throw new \RuntimeException("当前PHP环境不支持SHA256withRSA");
        }
        $url_parts = parse_url($url);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));

        //私钥地址 -- path/to/privateKey
        $mch_private_key = $this->mch_private_key;
//        return $mch_private_key;
        //商户号
        $merchant_id = $this->mchId;
        //当前时间戳
        $timestamp =  time();
        //随机字符串
        $nonce = $this->createNoncestr();
        //POST请求时 需要 转JSON字符串
        $this->body = !empty($data) ? json_encode($data) : '' ;
        $method = strtoupper($method);
        $message = $method."\n".
            $canonical_url."\n".
            $timestamp."\n".
            $nonce."\n".
            $this->body."\n";

        //生成签名
        openssl_sign($message, $raw_sign, openssl_get_privatekey(file_get_contents($mch_private_key)), 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);
//        return $sign;
        //Authorization 类型
        $schema = 'WECHATPAY2-SHA256-RSA2048';
        //生成token
        $token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $merchant_id,$this->mch_serial_no, $nonce, $timestamp,  $sign);

        //'User-Agent:*/*',
        $header = [
            'Content-Type:application/json',
            'Accept:application/json',
            'User-Agent : https://zh.wikipedia.org/wiki/User_agent',
            'Authorization: '.  $schema . ' ' . $token
        ];
        return $header;
    }

    /**
     * @Notes: 验签v3 Authorization
     *
     * @param: $sign 返回的签名串
     * @param: $data_str 构造的验签串
     * @param: $pub_key_file_path  -- 微信支付平台证书公钥路径
     * @return: bool
     * @author: Xuzhz 2021/5/28 11:31
     */
    public function checkAuthorization( $sign, $data_str, $pub_key_file_path='' ){
        if (!$pub_key_file_path ) {
            $pub_key_file_path = $this->platform_cert_pem;
        }
        $public_key = openssl_get_publickey( file_get_contents($pub_key_file_path) );
        if(empty($public_key)){
            return false;
        }
        $sign = base64_decode($sign);
        $ok = openssl_verify( $data_str, $sign, $public_key, OPENSSL_ALGO_SHA256 ); //SHA256
        openssl_free_key( $public_key );
        if ($ok == 1) {
            $result = true;
        } elseif ($ok == 0) {
            $result = false;
        } else {
            log_write('111DEBUG'. __CLASS__.' ' . __FUNCTION__ . ' 0 openssl_error_str '.json_encode(openssl_error_string()));
        }
        return $result;
    }

    /**
     *  作用:产生随机字符串,不长于32位
     */
    public function createNoncestr( $length = 32 )
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str ="";
        for ( $i = 0; $i < $length; $i++ )  {
            $str.= substr($chars, mt_rand(0, strlen($chars)-1), 1);
        }
        return $str;
    }

    /**
     * Decrypt AEAD_AES_256_GCM ciphertext  V3 -- 证书下载、回调报文解密
     * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
     * @param stingr    $aesKey             ApiV3_Key //商户需先在【商户平台】->【API安全】的页面设置该密钥
     * @param string    $associatedData     AES GCM additional authentication data
     * @param string    $nonceStr           AES GCM nonce
     * @param string    $ciphertext         AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    public function decryptToString($aesKey ,$associatedData, $nonceStr, $ciphertext)
    {
        if (strlen($aesKey) != 32 ) {
            throw new InvalidArgumentException('无效的ApiV3Key,长度应为32个字节');
        }

        $ciphertext = \base64_decode($ciphertext , true);
        if (strlen($ciphertext) <= 16) {
            return false;
        }

        // ext-sodium (default installed on >= PHP 7.2)
        if(function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available() ){
            return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
        }

        // ext-libsodium (need install libsodium-php 1.x via pecl)
        if(function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()){

            return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
        }

        // PHP >= 7.1
        if(PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods()) ){
            $ctext = substr($ciphertext, 0, -16);
            $authTag = substr($ciphertext, -16);
            return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr,$authTag, $associatedData);
        }

        throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }
    public function curlHttp( $url, $method='GET', $data=[], $headers=['Content-Type: application/json;charset=UTF-8'] )
    {
        $log = function ($info) {
            log_write($info, 'to_erp');
        };

        $json = json_encode($data);
        $log('request-url:' . $url);
        $log('request-data:' . $json);

        //弃用S::curl,会出现异常;
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        if( strtoupper($method) == 'POST' ) {
            curl_setopt($ch, CURLOPT_POST, TRUE);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
        }
        curl_setopt($ch, CURLOPT_HEADER, 1); //返回response头部信息
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers/*[
            'Content-Type: application/json;charset=UTF-8'
        ]*/);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible;)' );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );

        $beginMicroTime = microtime();
        $result         = curl_exec($ch);
//        print_r($result);

        //请求日志记录
        $endMicroTime = microtime();
        /*ApiRecord::dispatch(
            $url,
            curl_getinfo($ch, CURLINFO_HTTP_CODE),
            curl_error($ch),
            $json,
            $result,
            $beginMicroTime,
            $endMicroTime
        )->onQueue(ApiRecord::QUEUE_NAME);*/

        $log('response-data:' . $result);
        if( substr(curl_getinfo( $ch, CURLINFO_HTTP_CODE ), 0, 1) != '2' ){
            $errno = curl_errno( $ch );
            $log('response-code:' . $errno);
            curl_close( $ch );
            echo curl_getinfo( $ch, CURLINFO_HTTP_CODE ).PHP_EOL;
            return false;
        }
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $re_header = substr($result, 0, $headerSize);
        $log("返回的header头信息msg={$re_header}, url={$url}, data={$json}");
        $body = substr($result, $headerSize);

        //验签
        if(! $this->comboCheckAuth($re_header, $body)) {
            $log('验证失败');
            return false;
        }
        $log('验签成功');
//        echo 'comboCheckAuth验签成功';
        curl_close( $ch );
        if (!$result) {
            return false;
        }

//        $body = json_decode($result, 1);
        $body = $body ?? $result;
        if (is_string($body)) {
            $body = json_decode($body, 1);
            if (json_last_error() || !is_array($body)) {
                return false;
            } else {
                return $body;
            }
        } else if (is_array($body)) {
            return $body;
        } else {
            return false;
        }
    }

    // 回调通知,需要验签
    public function comboCheckAuth( $headers, $body )
    {
        $sign = $nonce = $timestamp = '';
        // 处理请求头
        $headArr = explode("\r\n", $headers);
        foreach ($headArr as $loop) {
            if (strpos($loop, "Wechatpay-Signature") !== false) {
                $sign = trim(substr($loop, strlen('Wechatpay-Signature')+2));
            }
            if (strpos($loop, "Wechatpay-Nonce") !== false) {
                $nonce = trim(substr($loop, strlen('Wechatpay-Nonce')+2));
            }
            if (strpos($loop, "Wechatpay-Timestamp") !== false) {
                $timestamp = trim(substr($loop, strlen('Wechatpay-Timestamp')+2));
            }
        }
        $message = $timestamp."\n".
            $nonce."\n".
            $body."\n";

        return $a = $this->checkAuthorization($sign, $message);
    }

    /**
     * 获取微信支付平台证书(微信支付负责申请), 与商户API证书(商户自行申请)不是一个内容
     * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_1.shtml
     *
     * @return 返回需要$this->decryptToString()解密
     * @return httpCode:200
     * @return {
    "data": [
    {
    "serial_no": "5157F09EFDC096DE15EBE81A47057A7232F1B8E1",
    "effective_time ": "2018-06-08T10:34:56+08:00",
    "expire_time ": "2018-12-08T10:34:56+08:00",
    "encrypt_certificate": {
    "algorithm": "AEAD_AES_256_GCM",
    "nonce": "61f9c719728a",
    "associated_data": "certificate",
    "ciphertext": "sRvt… "
    }
    },
    {...}
    ]
    }
     */
    public function getcertificates()
    {
        $url="https://api.mch.weixin.qq.com/v3/certificates";
        //生成V3请求 header认证信息
        $header = $this->createAuthorization( $url );
        $data = $this->curlHttp( $url, 'GET', $this->data , $header );

        //证书报文解密
        //$this->decryptToString();
        return $data;
    }
    /**
     * V3 -- 敏感信息(身份证、银行卡、手机号.etc)加密,微信支付平台证书中的(RSA)公钥加密
     * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_3.shtml
     */
    public function getEncrypt($str){
        //$str是待加密字符串
        $public_key_path = '证书地址'; //看情况使用证书, 个别接口证书 使用的是 平台证书而不是 api证书
        $public_key = file_get_contents($public_key_path);

        $encrypted = '';
        if (openssl_public_encrypt($str,$encrypted,$public_key,OPENSSL_PKCS1_OAEP_PADDING)) {
            //base64编码
            $sign = base64_encode($encrypted);
        } else {
            throw new Exception('encrypt failed');
        }
        return $sign;
    }
    /**
     * V3 -- 敏感信息(身份证、银行卡、手机号.etc)解密,微信商户私钥(RSA)解密
     * link: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_3.shtml
     */
    private function getDecrypt($str){
        //$str是待加密字符串
        $private_key_path = '平台证书路径';
        $private_key = file_get_contents($private_key_path);

        $dncrypted = '';
        if (openssl_private_decrypt(base64_decode($str),$dncrypted,$private_key,OPENSSL_PKCS1_OAEP_PADDING)) {
            # TODO
        } else {
            throw new Exception('dncrypt failed');
        }
        return $dncrypted;
    }
    //返回array | false
    protected function send($url, $method='GET', array $data=[])
    {
        // 生成V3请求 header认证信息
        $header = $this->createAuthorization($url, strtoupper($method), $data);
        // 发起curlHttp调用 & 验签
        $result = $this->curlHttp($url, strtoupper($method), $data , $header);

        return $result;
    }

    //微信api-v3上传图片
    public function wxPayUploadFile( )
    {
        header("Content-type:text/html;charset=utf-8");

        $url = 'https://api.mch.weixin.qq.com/v3/marketing/favor/media/image-upload';
        $filePath = APP_PATH . 'Public/Image/Wxzf/star-group.png';//'你需要上传的图片';
        $mime = mime_content_type($filePath);
        $filename = pathinfo($filePath, PATHINFO_BASENAME);

        //图片校验
        if(!$this->checkImgFile($filePath)){
            log_write('微信商户上传图片失败:'.$this->error);
            return false;
        }

        //私钥地址 -- path/to/privateKey
        $keyPath = $this->mch_private_key;
        //商户号
        $merchant_id = $this->mchId;

        $mess = $this->binaryEncodeImage($filePath);
        $filestr = json_encode(array('filename' => $filename, 'sha256' => hash_file("sha256", $filePath)));

        #准备参与签名数据
        $time = time();
        $nonce_str = $this->createNoncestr();
        $pkid = file_get_contents($keyPath);

        $token = $this->sign($url, "POST", $time, $nonce_str, $filestr, $pkid, $merchant_id, $this->mch_serial_no);

        //Authorization 类型
        $schema = 'WECHATPAY2-SHA256-RSA2048';
        #设置头部信息
        $boundary = '7derenufded';
        $headers = [
            "Authorization: " . $schema . ' ' . $token,
            "User-Agent:https://zh.wikipedia.org/wiki/User_agent",
            "Accept:application/json",
            "Content-Type:multipart/form-data;boundary=" . $boundary//切记boundary=后面这里切记这里不要加-- 和 “”
        ];

        #这里是构造请求body
        $boundarystr = "--{$boundary}\r\n";

        $out = $boundarystr;
        $out .= 'Content-Disposition:form-data;name="meta"' . "\r\n";#name必须是meta
        $out .= 'Content-Type: application/json; charset=UTF-8' . "\r\n";
        $out .= "\r\n";
        $out .= "" . $filestr . "\r\n";
        $out .= $boundarystr;
        $out .= 'Content-Disposition:form-data;name="file";filename="' . $filename . '"' . "\r\n";#name必须是file
        $out .= "Content-Type: {$mime}\r\n";
        $out .= "\r\n";
        $out .= $mess . "\r\n";
        $out .= "--{$boundary}--";

        // 发起curlMediaHttp调用 & 验签
        return $this->curlMediaHttp($url, 'POST', $out, $headers);
    }
    public function curlMediaHttp( $url, $method='GET', $out='', $headers=['Content-Type: application/json;charset=UTF-8'] )
    {
        $log = function ($info) {
            log_write($info, 'to_erp');
        };

        $json = $out;
        $log('request-url:' . $url);
        $log('request-data:' . $json);

        //弃用S::curl,会出现异常;
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
//        if( strtoupper($method) == 'POST' ) {
            curl_setopt($ch, CURLOPT_POST, TRUE);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
//        }
        curl_setopt($ch, CURLOPT_HEADER, 1); //返回response头部信息
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers/*[
            'Content-Type: application/json;charset=UTF-8'
        ]*/);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible;)' );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );

        $result         = curl_exec($ch);
//        var_dump($result);

        $log('response-data:' . $result);
        if( substr(curl_getinfo( $ch, CURLINFO_HTTP_CODE ), 0, 1) != '2' ){
            $errno = curl_errno( $ch );
            $log('response-code:' . $errno);
            curl_close( $ch );
            //echo curl_getinfo( $ch, CURLINFO_HTTP_CODE ).PHP_EOL;
            return false;
        }
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $re_header = substr($result, 0, $headerSize);
        $log("返回的header头信息msg={$re_header}, url={$url}, data={$json}");
        $body = substr($result, $headerSize);

        //验签
        if(! $this->comboCheckAuth($re_header, $body)) {
            $log('验证失败');
            return false;
        }
        $log('验签成功');
//        echo 'comboCheckAuth验签成功';
        curl_close( $ch );
        if (!$result) {
            return false;
        }

//        $body = json_decode($result, 1);
        $body = $body ?? $result;
        if (is_string($body)) {
            $body = json_decode($body, 1);
            if (json_last_error() || !is_array($body)) {
                return false;
            } else {
                return $body;
            }
        } else if (is_array($body)) {
            return $body;
        } else {
            return false;
        }
    }

    //签名
    private function sign($url,$http_method,$timestamp,$nonce,$body,$mch_private_key,$merchant_id,$serial_no){

        $url_parts = parse_url($url);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $message =
            $http_method."\n".
            $canonical_url."\n".
            $timestamp."\n".
            $nonce."\n".
            $body.
            "\n";
        openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);
        $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $merchant_id, $nonce, $timestamp, $serial_no, $sign);
        return $token;
    }

    /**
     * 图片转化为二进制数据流
     * @desc  图片转化为二进制数据流
     * return string
     */
    public function binaryEncodeImage($img_file){
        header("Content-type:text/html;charset=utf-8");
        $p_size = filesize($img_file);
        $img_binary = fread(fopen($img_file, "rb"), $p_size);
        return $img_binary;
    }

    //判断图片类型
    public function checkImgFile($file)
    {
        if(!is_file($file)){
            $this->error='无效文件';
            return false;
        }
        $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
        if (function_exists('exif_imagetype')) {
            $imgType=exif_imagetype($file);
        }else{
            $res = getimagesize($file);
            $imgType=$res?$res[2]:false;
        }
        if (!in_array($extension, ['jpg', 'jpeg', 'bmp', 'png']) || !in_array($imgType, [ 2, 3, 6])) {
            $this->error='无效图片文件';
            return false;
        }

        if(filesize($file) > 2097152){
            $this->error='图片文件大小超过 2M';
            return false;
        }

        return true;
    }

5、over!

 

posted @ 2021-06-08 14:43  PHP急先锋  阅读(1757)  评论(0编辑  收藏  举报