巨坑的微信v3支付之平台证书篇

今天开发遇到一个奇葩的Bug,微信支付返回下面的错误信息

Cannot found the serial(`2ED385C4E1BB5C*****0`)'s configuration, which's from the response(header:Wechatpay-Serial), your's 71BA423BF5AACA9530*******50.

经过排查,发现是平台证书错了。经过查询原因是,不同的微信支付使用的并不是同一个平台证书。

首页在微信支付的API安全,中查看下自己的平台证书序号,如下图:

红框内是平台证书,点击管理证书,可以看到证书的序号。错误信息中第一个括号中的2ED38***这个就是应该使用的平台证书序号,而下面的your's 71B ,这个是你当前使用的错误的证收。

原因讲完了。

下面说下我的使用场景:

因为我们是SAAS系统,会有多个商户使用不同的商户证书。

所以需要根据不同的商户选择不同的平台证书。那就需要下载各自的证书。

下载证书办法:


class CertDown{
    private $v3Key = '';
    private  $certPath = '';

    private $keyPath = '';
    private $mchId = '';
    private $keySerial = '';

    private $appId;

    private $certContent = '';

    /**
     * @param $appId 公众号或小程序的appID
     * @param $v3Key v3支付的key
     * @param $mchId 商户ID
     * @param $keySerial 私钥证书编号
     * @param $certPath 这个参数没有用到
     * @param $keyPath 私钥证书存储路径
     */
    public function __construct($appId,$v3Key,$mchId,$keySerial,$certPath,$keyPath){
        $this->v3Key    = $v3Key;
        $this->certPath = $certPath;
        $this->keyPath  = $keyPath;

        $this->mchId = $mchId;

        $this->keySerial = $keySerial;

        $this->appId = $appId;
    }
    /**
     * 获取证书
     * @return mixed
     */
    public function certificates(){
        //请求参数(报文主体)
        $headers = $this->sign('GET','https://api.mch.weixin.qq.com/v3/certificates','');
        $result = $this->curl_get('https://api.mch.weixin.qq.com/v3/certificates',$headers);
        $result = json_decode($result,true);
        //print_r($result);exit();
        $aa = $this->decryptToString($result['data'][0]['encrypt_certificate']['associated_data'],$result['data'][0]['encrypt_certificate']['nonce'],$result['data'][0]['encrypt_certificate']['ciphertext']);
        $this->certContent = $aa;
        //echo($aa);//解密后的内容,就是证书内容
    }

    public function save($shopId){
        $path = public_path() .'uploads/platform_'.$shopId .'.pem';
        echo $path;
        return file_put_contents($path,$this->certContent);

    }
    /**
     * 签名
     * @param string $http_method    请求方式GET|POST
     * @param string $url            url
     * @param string $body           报文主体
     * @return array
     */
    public function sign($http_method = 'POST',$url = '',$body = ''){
        $mch_private_key = $this->getMchKey();//私钥
        $timestamp = time();//时间戳
        $nonce = $this->getRandomStr(32);//随机串
        $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);
        //设置HTTP头
        $config = $this->config();
        $token = sprintf('WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $config['mchid'], $nonce, $timestamp, $config['serial_no'], $sign);
        $headers = [
            'Accept: application/json',
            'User-Agent: */*',
            'Content-Type: application/json; charset=utf-8',
            'Authorization: '.$token,
        ];
        return $headers;
    }
    //私钥
    public function getMchKey(){
        //path->私钥文件存放路径
        return openssl_get_privatekey(file_get_contents($this->keyPath));
    }
    /**
     * 获得随机字符串
     * @param $len      integer       需要的长度
     * @param $special  bool      是否需要特殊符号
     * @return string       返回随机字符串
     */
    public function getRandomStr($len, $special=false){
        $chars = array(
            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
            "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
            "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
            "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
            "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
            "3", "4", "5", "6", "7", "8", "9"
        );

        if($special){
            $chars = array_merge($chars, array(
                "!", "@", "#", "$", "?", "|", "{", "/", ":", ";",
                "%", "^", "&", "*", "(", ")", "-", "_", "[", "]",
                "}", "<", ">", "~", "+", "=", ",", "."
            ));
        }

        $charsLen = count($chars) - 1;
        shuffle($chars);                            //打乱数组顺序
        $str = '';
        for($i=0; $i<$len; $i++){
            $str .= $chars[mt_rand(0, $charsLen)];    //随机取出一位
        }
        return $str;
    }
    /**
     * 配置
     */
    public function config(){
        return [
            'appid' => $this->appId,
            'mchid' => $this->mchId,//商户号
            'serial_no' => $this->keySerial,//证书序列号
            'description' => '描述',//应用名称(随意)
            'notify' => '',//支付回调
        ];
    }
//get请求
    public function curl_get($url,$headers=array())
    {
        $info = curl_init();
        curl_setopt($info,CURLOPT_RETURNTRANSFER,true);
        curl_setopt($info,CURLOPT_HEADER,0);
        curl_setopt($info,CURLOPT_NOBODY,0);
        curl_setopt($info,CURLOPT_SSL_VERIFYPEER,false);
        curl_setopt($info,CURLOPT_SSL_VERIFYPEER,false);
        curl_setopt($info,CURLOPT_SSL_VERIFYHOST,false);
        //设置header头
        curl_setopt($info, CURLOPT_HTTPHEADER,$headers);
        curl_setopt($info,CURLOPT_URL,$url);
        $output = curl_exec($info);
        curl_close($info);
        return $output;
    }

    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;
    /**
     * Decrypt AEAD_AES_256_GCM ciphertext
     *
     * @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($associatedData, $nonceStr, $ciphertext) {
        $aesKey = $this->v3Key;
        $ciphertext = \base64_decode($ciphertext);
        if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
            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);
        }

        // openssl (PHP >= 7.1 support AEAD)
        if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
            $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
            $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

            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');
    }

}

上面是下载证书的类,然后执行下载:

 $down = new CertDown($config['appid'],$config['v3paykey'],$config['mchid'],$config['cert_serial'],'',$apiclientKey);
                $down->certificates();
                $res = $down->save($shopId);

这时候我们在调用平台证书的时候,使用每个商户对应的平台证书就可以了。

把这里的证书换成新的证书就可以了。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值