今天开发遇到一个奇葩的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);
这时候我们在调用平台证书的时候,使用每个商户对应的平台证书就可以了。
把这里的证书换成新的证书就可以了。