微信小微商户下载平台证书接口(PHP SHA256 with RSA 签名,AEAD_AES_256_GCM解密方法)
一、序言
最近在做微信小微商户接口对接,对接里面的下载平台证书接口中遇到的坑在这记录下。
二、资料
1、《1.1. 下载平台证书接口(v5.1)》 查看
二、正文
小微商户申请入驻接口中有几个参数是需要先调用下载证书接口的,所以我们现在先看看下载证书接口。小微商户接口PHP相关的示例代码,网上也找不到什么例子。一个人研究这个下载证书接口也踩了好多坑,现在发出来,希望之后做这个接口的人百度的时候有个参考。
当我请求接口时一直返回签名失败,对着文档看了几遍都没发现错在哪。 看下图计算签名值方法中构造待签名串这个地方,我按他的第一步第二步来拼接串,后来发现他最下面最终的拼接不是按这个第一步第二步来的。最终的位置应该是第四步请求时间戳和第三步请求随机串应该是调换位置的。然后拿这个值去生成签名就OK了。
接口中主要是没有PHP示例,而且平时很少用到文档中的加密签名解密等方法,一时有点懵逼。后来经过一番查找文档手册,在 php.net 中找到了答案。具体写法我就不一一赘述,大家可看下下面贴出的容易采坑几个方法,加密方法里面的函数有兴趣的可以自行去百度。
三、下载证书接口难点示例
(1) 准备接口中使用到的加密等方法
/**
* getRandChar 获取随机字符串
* @param int $length
* @return mixed
*/
abstract protected function getRandChar($length = 32);
/**
* setHashSign SHA256 with RSA 签名
* @param $signContent
* @return string
*/
protected function encryptSign($signContent)
{
// 解析 key 供其他函数使用。
$privateKey = openssl_get_privatekey($this->getPrivateKey());
// 调用openssl内置签名方法,生成签名$sign
openssl_sign($signContent, $sign, $privateKey, "SHA256");
// 释放内存中私钥资源
openssl_free_key($privateKey);
$sign = base64_encode($sign);
return $sign;
}
(2)curl请求下载证书接口
/**
* getCertificates 下载平台证书
* @return mixed
*/
public function downloadCertificates()
{
try {
$url = self::WXAPIHOST . 'v3/certificates';
// 请求随机串
$nonce_str = $this->getRandChar();
// 当前时间戳
$timestamp = time();
// 签名串
$signContent = "GET\n/v3/certificates\n" . $timestamp . "\n" . $nonce_str . "\n\n";
// 签名值
$signature = $this->encryptSign($signContent);
// 含有服务器用于验证商户身份的凭证
$authorization = 'WECHATPAY2-SHA256-RSA2048 mchid="' . $this->mch_id . '",nonce_str="' . $nonce_str . '",signature="' . $signature . '",timestamp="' . $timestamp . '",serial_no="' . $this->serial_no . '"';
$curl_v = curl_version();
$header = [
'Accept:application/json',
// 'Accept-Language:zh-CN', // 默认 zh-CN 可以不填
'Authorization:' . $authorization,
'Content-Type:application/json',
'User-Agent:curl/' . $curl_v['version'],
];
$result = $this->httpsRequest($url, NULL, $header);
$responseHeader = $this->parseHeaders($result[2]);
$http_code = $result[1];
$responseBody = json_decode($result[0], true);
if ($http_code == 200 && !isset($responseBody['code'])) {
return $this->verifySign($responseHeader, $result[0]);
} else {
throw new \Exception($responseBody['code'] . '----' . $responseBody['message']);
}
} catch (\Exception $e) {
throw new WxException($e->getCode());
}
}
(3)curl需要获取响应头然后校验响应头里面的签名以及解密返回的密文证书
/**
* verifyHashSign 校验签名
* @param $data
* @param $signature
* @return int
*/
protected function verifySign($responseHeader, $responseBody)
{
$last_data = $this->newResponseData();
$new_data = json_decode($responseBody, true);
$one = false;
if (empty($last_data)) {
// 没有获取到上一次保存在本地的数据视为第一请求下载证书接口
$serial_no = $this->getNewCertificates($new_data['data']);
$one = true;
} else {
$serial_no = $last_data['serial_no'];
}
// 注 1:微信支付平台证书***位于 HTTP 头`Wechatpay-Serial`,验证签名前请先检查***是否跟商户所持有的微信支付平台证书***一致。(第一次从 1.1.5.中回包字段 serial_no 获取,非第一次时使用上次本地保存的平台证书***)
if ($serial_no != $responseHeader['Wechatpay-Serial']) {
if ($one)
$this->clearFile('newResponseData');
return 0;
}
$publicKey = $this->getPublicKey();
if ($publicKey) {
// 用微信支付平台证书公钥(第一次下载平台证书时从 1.1.5.中 “加密后的证书内容”进行解密获得。非第一次时使用上次本地保存的公钥)对“签名串”进行 SHA256 with RSA 签名验证
$data = $this->signatureValidation($responseHeader, $responseBody);
$signature = base64_decode($responseHeader['Wechatpay-Signature']);
$publicKeyResource = openssl_get_publickey($publicKey);
$f = openssl_verify($data, $signature, $publicKeyResource, "SHA256");
openssl_free_key($publicKeyResource);
if ($f == 1 && !empty($last_data)) {
// 获取弃用日期最长证书
return $this->getNewCertificates($new_data['data'], $last_data);
}
return $f;
} else {
return 0;
}
}
/**
* decryptCiphertext AEAD_AES_256_GCM 解密加密后的证书内容得到平台证书的明文
* @param $ciphertext
* @param $ad
* @param $nonce
* @return string
*/
protected function decryptCiphertext($data)
{
$encryptCertificate = $data['encrypt_certificate'];
$ciphertext = base64_decode($encryptCertificate['ciphertext']);
$associated_data = $encryptCertificate['associated_data'];
$nonce = $encryptCertificate['nonce'];
// sodium_crypto_aead_aes256gcm_decrypt >=7.2版本,去php.ini里面开启下libsodium扩展就可以,之前版本需要安装libsodium扩展,具体查看php.net(ps.使用这个函数对扩展的版本也有要求哦,扩展版本 >=1.08)
$plaintext = sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associated_data, $nonce, $this->aes_key);
$this->savePublicKey($plaintext);
$this->newResponseData($data);
return true;
}
四、结束语
其实这个接口本身不难,主要是其中使用的PHP SHA256 with RSA 签名、AEAD_AES_256_GCM解密方法等之前没有接触过,而网上相关文章又是比较少,所以做的过程中踩了不少坑。希望看到本篇文章能够帮助在看的朋友少踩点坑。有疑问可以在下面评论区评论留言
上一篇: C算法:插入排序(内附详细图解)!!!
下一篇: 一致性哈希