吐槽一下微信支付的服务端SDK 接口 (SDK - SDK&开发工具 | 微信支付商户文档中心):代码写的确实好很OO、很优雅,额,但也不见的得写好,那哪里是接口,就是个显摆自己加密技术封装写的多好多好.
以PHP的来说 (wechatpay/wechatpay - Packagist ):
1、一个PublicKey还要用户用工具自己去下载.(浏览器访问https我们得手动下载证书?)
2、这SDK哪里是接口,没有整合一个接口,就只是做个加解密的封装罢了,封装完还是给出的是PSR的消息...
3、敏感内容要加解密还得写代码处理,就不能自动处理一下,或暴露个方法自动识别一下么.
哎,在他之上这再整个PHP封装函数,希望新手不那么迷茫:对开发人员来讲,已知什么就直接用什么就行了:
已知我们就只有这些,所以就不要再折磨我们做别的了:
商户号
商户API私钥文件路径 (/certs/apiclient_key.pem)
商户API证书文件路径 (/certs/apiclient_cert.pem)
商户API证书V3 key
这些都在 https://pay.weixin.qq.com/index.php/core/cert/api_cert 可以获取到,或设置
那我们是不是就这样就够了:
public static function NewClient(
string $merchantId,
string $merchantPrivateKeyFilePath,
string $merchantCertFilePath,
string $apiv3Key
)
接着开始用,怎么用:
比如我们添加一个分账的账号,这里用到了加密/解密:
$client = $this->NewClient($merchantId, $merchantPrivateKeyFilePath, $merchantCertFilePath, $apiv3Key);
$resp = $client->chain('/v3/profitsharing/receivers/add')
->post([
'json' => [
'appid' => $appId,
'type' => 'PERSONAL_OPENID', // 个人OpenID| PERSONAL_OPENID; 商户号| MERCHANT_ID; 个人在子商户应用下的OpenID|PERSONAL_SUB_OPENID
'account' => 'oITsy5K-EfWhvRJavlm35CRap4Ac',
//要加密
'name' => $client->enc('谢平康'), //【分账接收方全称】,这个是要加秘的
'relation_type' => 'PARTNER',//SERVICE_PROVIDER:服务商 STORE:门店 STAFF:员工 STORE_OWNER:店主 PARTNER:合作伙伴 HEADQUARTER:总部
// BRAND:品牌方 DISTRIBUTOR:分销商 USER:用户 SUPPLIER:供应商 CUSTOM:自定义
'custom_relation' => null, //自定义的分账关系
'description' => '平台技术支持',
]])->getBody();
//返回的结果
echo $resp, PHP_EOL;
//结果识别处理
$data = json_decode($resp);
//返回的结果解密一下名称
echo($client->dec($data->name)), PHP_EOL;
什么公钥证书下载的都我们没关系,
可以简单做个cache存一下证书,根据证书的有效期定期更新证书就行了
直接上代码,函数写好一个方法就够用了:
上代码:
/**
* 创建一个新的客户端实例。
*
* @param string $merchantId 商户号
* @param string $merchantPrivateKeyFilePath 商户API私钥文件路径
* @param string $merchantCertFilePath 商户API证书文件路径
* @param string $apiv3Key 商户API证书V3 key
* @param string $platformCertificateFilePath 平台的公钥文件保存路径,可不传默认当前目录下 ( __DIR__ . '/webchat_pay_public-key.pem')
* @return object 返回客户端实例或带有加密功能的客户端实例
*/
public static function NewClient(
string $merchantId,
string $merchantPrivateKeyFilePath,
string $merchantCertFilePath,
string $apiv3Key,
string $platformCertificateFilePath = null
)
{
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 「商户API证书」的「证书序列号」
//可以用 openssl x509 -noout -serial -in apiclient_cert.pem 查看到,这个直接用代码获取就行了
$merchantCertificateSerial = PemUtil::parseCertificateSerialNo($merchantCertFilePath);
//没传公钥文件就给他一下默认当前目录下
if (!$platformCertificateFilePath) {
$platformCertificateFilePath = 'file://' . __DIR__ . '/webchat_pay_public-key.pem';
}
//判断证书是否过期
if (file_exists($platformCertificateFilePath)) {
$certificate = openssl_x509_read(file_get_contents($platformCertificateFilePath));
if (!$certificate) {
//证书不对,删除文件
unlink($platformCertificateFilePath);
} else {
// 解析证书以获取有效期
$parsedCertificate = openssl_x509_parse($certificate);
$validFrom = $parsedCertificate['validFrom_time_t'];
$validTo = $parsedCertificate['validTo_time_t'];
$currentTime = time();
// 检查证书不在有效期内再重新下载
if ($validFrom > $currentTime || $validTo < $currentTime) {
unlink($platformCertificateFilePath);
}
}
}
//判断文件是否存在或过期,要重新下载
if (!file_exists($platformCertificateFilePath)) {
//下载证书
static $certs = ['any' => null];
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'certs' => &$certs,
'privateKey' => $merchantPrivateKeyInstance,]);
$stack = $instance->getDriver()->select('v3')->getConfig('handler');
$stack->after('verifier', function (callable $handler) use ($apiv3Key, &$certs) {
return function ($request, array $options) use ($apiv3Key, $handler, &$certs) {
return $handler($request, $options)->then(function (ResponseInterface $response) use ($apiv3Key, $request, &$certs) {
$body = (string)$response->getBody();
$json = \json_decode($body);
$data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
\array_map(static function ($row) use ($apiv3Key, &$certs) {
$cert = $row->encrypt_certificate;
$certs[$row->serial_no] = AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data);
}, $data);
return $response;
});
};
}, 'injector');
$response = $instance->chain('v3/certificates')->get();
$body = (string)$response->getBody();
$json = \json_decode($body);
$data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
foreach ($data as $row) {
$serialNo = $row->serial_no;
\file_put_contents($platformCertificateFilePath, $certs[$serialNo]);
}
}
// 从本地文件中加载「微信支付平台证书」(可使用证书下载工具得到),用来验证微信支付应答的签名
$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 从「微信支付平台证书」中获取「证书序列号」
$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
// 构造一个 APIv3 客户端实例
$config = [
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $platformPublicKeyInstance,
],
'headers' => [
'Wechatpay-Serial' => $platformCertificateSerial,
]
];
$instance = Builder::factory($config);
// 做一个匿名类返回,也可以用简单的 array
return new class($instance, $platformPublicKeyInstance, $merchantPrivateKeyInstance) {
public $instance;
private $platformPublicKeyInstance;
private $merchantPrivateKeyInstance;
public function __construct($instance, $platformPublicKeyInstance, $merchantPrivateKeyInstance)
{
$this->instance = $instance;
$this->platformPublicKeyInstance = $platformPublicKeyInstance;
$this->merchantPrivateKeyInstance = $merchantPrivateKeyInstance;
}
public function enc(string $msg): string
{
return Rsa::encrypt($msg, $this->platformPublicKeyInstance);
}
public function chain(string $msg)
{
return $this->instance->chain($msg);
}
public function dec(string $msg): string
{
return Rsa::decrypt($msg, $this->merchantPrivateKeyInstance);
}
};
}
最后还得记录引入下:
use Psr\Http\Message\ResponseInterface;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;