Commit da14de52 authored by wangzhengwen's avatar wangzhengwen

wehcatpay

parent dd416904
composer exec CertificateDownloader.php -- -k "VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA" -m "1716540652" -f "D:/work/code/projecttwo/config/cert/wechat/apiclient_key.pem" -s "113BF40FB7EFF4C2BA69166A211A4570E969AC01" -o "D:/work/code/projecttwo/config/cert/wechat"
php ./bin/CertificateDownloader.php -k "VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA" -m "1716540652" -f "D:/work/code/projecttwo/config/cert/wechat/apiclient_key.pem" -s "113BF40FB7EFF4C2BA69166A211A4570E969AC01" -o "D:/work/code/projecttwo/config/cert/wechat"
/www/server/php/82/bin/php/ ./bin/CertificateDownloader.php ./bin/CertificateDownloader.php -k "VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA" -m "1716540652" -f "/www/wwwroot/api.kjxl.zhubei.cn/config/cert/wechat/apiclient_key.pem" -s "113BF40FB7EFF4C2BA69166A211A4570E969AC01" -o "/www/wwwroot/api.kjxl.zhubei.cn/config/cert/wechat"
......@@ -9,10 +9,11 @@ use Yansongda\Pay\Pay;
use app\api\service\PayService;
use think\facade\Db;
use think\App;
use app\api\service\WeChatPayService;
class PayController
{
//--ignore-platform-req=ext-swoole
/**
* 创建支付订单
......@@ -107,7 +108,7 @@ class PayController
if (!$payment || $payment['pay_method'] != PayService::PAY_METHOD_WECHAT) {
throw new \Exception("支付订单不存在或支付方式不匹配");
}
// halt(intval($payment['pay_amount'] * 100));
// 构造微信订单
$order = [
'out_trade_no' => $orderNo,
......@@ -117,17 +118,10 @@ class PayController
'total' => intval($payment['pay_amount'] * 100), // 转为分
],
];
$wechatPay = new WeChatPayService();
$result = $wechatPay->createNativeOrder($order['out_trade_no'], $order['amount']['total'], $order['description']);
return json(['code' => 1, 'msg' =>'success','data'=>$result]);
// 发起支付
$response = Pay::wechat(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT))->scan($order);
return json([
'code' => 200,
'data' => [
'code_url' => $response->code_url,
'order_no' => $orderNo
]
]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
......@@ -162,22 +156,19 @@ class PayController
*/
public function wechatNotify()
{
$wechat = Pay::wechat(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT));
try {
$data = $wechat->verify();
// 处理支付回调
$result = PayService::handlePaymentNotify($data->out_trade_no, $data->all());
if ($result) {
return $wechat->success();
} else {
return json(['code' => 500, 'msg' => '处理失败']);
$wechatPayService = new WeChatPayService();
$res = $wechatPayService->handleNotify();
if($res)
{
return json(['code' => 'success', 'msg' => '成功']);
}
return json(['code' => 'FAIL', 'msg' => '失败'],500);
} catch (\Exception $e) {
Log::error('微信回调异常: ' . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
return json(['code' => 'FAIL', 'msg' => '失败'],500);
}
}
......
<?php
namespace app\api\controller;
use app\api\middleware\Auth;
use app\BaseController;
use think\facade\Log;
use think\facade\View;
use Yansongda\Pay\Pay;
use app\api\service\PayService;
use think\facade\Db;
use think\App;
class PayController_old
{
//--ignore-platform-req=ext-swoole
/**
* 创建支付订单
*/
public function create($params)
{
// $params = request()->only([
// 'order_id',
// 'order_type',
// 'pay_method',
// 'amount',
// 'user_id'
// ]);
//
// // 验证参数
// $validate = validate('Pay');
// if (!$validate->scene('create')->check($params)) {
// return json(['code' => 400, 'msg' => $validate->getError()]);
// }
// 创建支付订单
$result = PayService::createPayment(
$params['user_id'],
$params['order_id'],
$params['amount'],
$params['pay_method'],
$params['order_type'],
request()->param()
);
if (!$result['status']) {
return json(['code' => 0, 'msg' => $result['msg']]);
}
return json([
'code' => 1,
'data' => [
'payment_id' => $result['data']['payment_id'],
'order_no' => $result['data']['order_no']
]
]);
}
/**
* 发起支付宝支付
*/
public function alipay()
{
$orderNo = request()->param('order_no');
try {
// 查询支付订单
$payment = Db::name('payment')
->where('order_no', $orderNo)
->find();
if (!$payment || $payment['pay_method'] != PayService::PAY_METHOD_ALIPAY) {
throw new \Exception("支付订单不存在或支付方式不匹配");
}
// 构造支付宝订单
$order = [
'out_trade_no' => $orderNo,
'total_amount' => $payment['pay_amount'],
'subject' => $payment['order_type'] == PayService::ORDER_TYPE_COURSE ?
'课程购买' : '证书购买',
];
// 发起支付
$alipay = Pay::alipay(PayService::getPayConfig(PayService::PAY_METHOD_ALIPAY))->web($order);
halt($alipay);
return redirect($alipay->getTargetUrl());
} catch (\Exception $e) {
Log::error("支付创建错误: {$orderNo} - " . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 发起微信支付
*/
public function wechat()
{
$orderNo = request()->param('order_no');
try {
// 查询支付订单
$payment = Db::name('payment')
->where('order_no', $orderNo)
->find();
if (!$payment || $payment['pay_method'] != PayService::PAY_METHOD_WECHAT) {
throw new \Exception("支付订单不存在或支付方式不匹配");
}
// 构造微信订单
$order = [
'out_trade_no' => $orderNo,
'description' => $payment['order_type'] == PayService::ORDER_TYPE_COURSE ?
'课程购买' : '证书购买',
'amount' => [
'total' => intval($payment['pay_amount'] * 100), // 转为分
],
];
Pay::setContainer(\Illuminate\Container\Container::getInstance());
// Pay::config(config('pay.wechat'));
$result = Pay::wechat()->scan($order);
halt($result);
// halt(config('pay.wechat'));
// halt(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT)['default']);
// 发起支付
// $response = Pay::wechat(config('pay.wechat'))->scan($order);
$pay = Pay::wechat()->scan($order);
dd($pay); // 查看最终生成的配置
return json([
'code' => 200,
'data' => [
'code_url' => $response->code_url,
'order_no' => $orderNo
]
]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 支付宝回调处理
*/
public function alipayNotify()
{
$alipay = Pay::alipay(PayService::getPayConfig(PayService::PAY_METHOD_ALIPAY));
try {
$data = Pay::alipay()->callback();
// 处理支付回调
$result = PayService::handlePaymentNotify($data->out_trade_no, $data->all());
if ($result) {
return $alipay->success();
} else {
return json(['code' => 500, 'msg' => '处理失败']);
}
} catch (\Exception $e) {
Log::error('支付宝回调异常: ' . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 微信支付回调处理
*/
public function wechatNotify()
{
$wechat = Pay::wechat(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT));
try {
$data = $wechat->verify();
// 处理支付回调
$result = PayService::handlePaymentNotify($data->out_trade_no, $data->all());
if ($result) {
return $wechat->success();
} else {
return json(['code' => 500, 'msg' => '处理失败']);
}
} catch (\Exception $e) {
Log::error('微信回调异常: ' . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 查询支付状态
*/
public function query()
{
$orderNo = request()->param('order_no');
try {
$payment = Db::name('fj_payment')
->where('order_no', $orderNo)
->find();
if (!$payment) {
throw new \Exception("支付订单不存在");
}
return json([
'code' => 200,
'data' => [
'order_no' => $payment['order_no'],
'pay_status' => $payment['pay_status'],
'pay_time' => $payment['pay_time'],
'pay_amount' => $payment['pay_amount']
]
]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
}
\ No newline at end of file
......@@ -9,11 +9,7 @@ use app\model\system\SystemArea;
class Util extends BaseController
{
protected $middleware = [
[
'Auth' => ['except' => ['sums']]
],
];
public function getAreaJson()
{
......
......@@ -88,10 +88,11 @@ class PayService
*/
public static function handlePaymentNotify($orderNo, $notifyData)
{
Db::startTrans();
try {
// 查询支付订单
$payment = Db::name('fj_payment')
$payment = Db::name('payment')
->where('order_no', $orderNo)
->lock(true)
->find();
......@@ -108,7 +109,7 @@ class PayService
// 验证金额
$amount = $payment['pay_method'] == self::PAY_METHOD_ALIPAY ?
$notifyData['total_amount'] : ($notifyData['total_fee'] / 100);
$notifyData['amount']['total'] : ($notifyData['amount']['total'] / 100);
if (bccomp($amount, $payment['pay_amount'], 2) !== 0) {
throw new \Exception("金额不一致: 订单{$payment['pay_amount']}, 回调{$amount}");
......@@ -122,7 +123,7 @@ class PayService
'updatetime' => time()
];
$result = Db::name('fj_payment')
$result = Db::name('payment')
->where('order_no', $orderNo)
->update($updateData);
......
<?php
namespace app\api\service;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;
use think\facade\Config;
use think\facade\Log;
use veitool\qrcode\QRcode;
use WeChatPay\Formatter;
use WeChatPay\Crypto\AesGcm;
use think\Response;
class WeChatPayService
{
protected $instance;
public function __construct()
{
$config = Config::get('pay.wechat.default');
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyFilePath = 'file://' . $config['private_key_path'];
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 「商户API证书」的「证书序列号」
//从本地文件中加载「微信支付公钥」,用来验证微信支付应答的签名
$platformPublicKeyFilePath = 'file://' . $config['publick_key_path'];
$twoPlatformPublicKeyInstance = Rsa::from($platformPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC);
// halt($merchantPrivateKeyInstance);
// 「微信支付公钥」的「微信支付公钥ID」
// 需要在 商户平台 -> 账户中心 -> API安全 查询
$platformPublicKeyId = $config['publick_key_id'];
// 从本地文件中加载「微信支付平台证书」,可由内置CLI工具下载到,用来验证微信支付应答的签名
$platformCertificateFilePath = 'file://' . $config['wechatpay_platform_path'];
$onePlatformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
$wechatpay_platform_id = $config['wechatpay_platform_id'];
// 构造一个APIv3的客户端实例
$this->instance = Builder::factory([
'mchid' => $config['mchid'],
'serial' => $config['serial_no'],
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$wechatpay_platform_id => $onePlatformPublicKeyInstance,
$platformPublicKeyId => $twoPlatformPublicKeyInstance,
],
]);
// try {
// $resp = $this->instance->chain('v3/certificates')->get(
// /** @see https://docs.guzzlephp.org/en/stable/request-options.html#debug */
// // ['debug' => true] // 调试模式
// );
// echo (string) $resp->getBody(), PHP_EOL;
// } catch(\Exception $e) {
// // 进行异常捕获并进行错误判断处理
// echo $e->getMessage(), PHP_EOL;
// if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
// $r = $e->getResponse();
// echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
// echo (string) $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
// }
// echo $e->getTraceAsString(), PHP_EOL;
// }
// exit();
}
private function loadPlatformCertificates()
{
// 获取当前所有平台证书
$certificates = $this->instance->v3->certificates->get();
// halt($certificates['data']);
foreach ($certificates['data'] as $cert) {
// 解密证书
$plainCert = AesGcm::decrypt(
$cert['encrypt_certificate']['ciphertext'],
config('pay.wechat.default.apiv3_key'),
$cert['encrypt_certificate']['nonce'],
$cert['encrypt_certificate']['associated_data']
);
// 解析序列号和公钥
$serialNo = $cert['serial_no'];
$publicKey = Rsa::from($plainCert, Rsa::KEY_TYPE_PUBLIC);
// 添加到配置
$this->instance->getConfig()->set('certs', [
$serialNo => $publicKey
]);
}
}
/**
* 创建扫码支付订单
* @param string $outTradeNo 商户订单号
* @param int $amount 金额(分)
* @param string $description 商品描述
* @return array
*/
public function createNativeOrder($outTradeNo, $amount, $description)
{
$config = Config::get('pay.wechat.default');
try {
$resp = $this->instance
->chain('v3/pay/transactions/native')
->post(['json' => [
'mchid' => $config['mchid'],
'appid' => $config['appid'],
'description' => $description,
'out_trade_no' => $outTradeNo,
'notify_url' => $config['notify_url'],
'amount' => [
'total' => (int)$amount,
'currency' => 'CNY',
],
]]);
$body = json_decode((string)$resp->getBody(), true);
$codeUrl = $body['code_url']; // 提取支付链接
$qr = new QRcode();
$qr_image = $qr->png($codeUrl)->getBase64();
return [
'code' => 1,
'data' => [
'code_url' => $codeUrl, // 支付链接
'qr_image' => $qr_image
]
];
} catch (\Exception $e) {
// halt($e->getCode());
return ['code' => 0, 'msg' => $e->getMessage()];
}
}
/**
* 处理支付通知
*/
public function handleNotify()
{
try {
$config = Config::get('pay.wechat.default');
// 获取微信回调的原始数据
$inWechatpaySignature = request()->header('Wechatpay-Signature');
$inWechatpayTimestamp = request()->header('Wechatpay-Timestamp');
$inWechatpaySerial = request()->header('Wechatpay-Serial');
$inWechatpayNonce = request()->header('Wechatpay-Nonce');
$header = request()->header();
$body = file_get_contents('php://input');
Log::info('微信支付回调原始数据: ' . json_encode([
'headers' => [
'Wechatpay-Signature' => $inWechatpaySignature,
'Wechatpay-Timestamp' => $inWechatpayTimestamp,
'Wechatpay-Serial' => $inWechatpaySerial,
'Wechatpay-Nonce' => $inWechatpayNonce
],
'body' => $body
]));
$apiv3Key = $config['apiv3_key'];
$platformPublicKeyInstance = Rsa::from('file://' . $config['wechatpay_platform_path'], Rsa::KEY_TYPE_PUBLIC);
$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
$verifiedStatus = Rsa::verify(
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $body),
$inWechatpaySignature,
$platformPublicKeyInstance
);
if (!$verifiedStatus) {
// throw new \Exception("签名验证失败:");
}
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($body, true);
// 使用PHP7的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
// 加密文本消息解密
$inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
$inBodyResourceArray = (array)json_decode($inBodyResource, true);
$res = $this->queryOrderByOutTradeNo($inBodyResourceArray['out_trade_no']);
$res = json_decode($res, true);
if ($res['trade_state'] == 'SUCCESS') {
// 处理业务逻辑
$res = PayService::handlePaymentNotify($inBodyResourceArray['out_trade_no'], $inBodyResourceArray);
return $res;
}else {
return false;
}
// 返回成功响应
return true;
} catch (\Exception $e) {
Log::error("微信支付回调失败:{$e->getMessage()}");
return false;
}
}
/**
* 根据商户订单号查询支付订单
* @param string $outTradeNo 商户订单号
* @return array
* @throws \Exception
*/
public function queryOrderByOutTradeNo(string $outTradeNo)
{
try {
$resp = $this->instance
->chain("v3/pay/transactions/out-trade-no/{$outTradeNo}?mchid=" . Config::get('pay.wechat.default.mchid'))
->get(['mchid' => Config::get('pay.wechat.default.mchid')]);
// 如果是 PSR-7 响应,获取错误详情
if (method_exists($resp, 'getStatusCode') && $resp->getStatusCode() !== 200) {
$errorBody = $resp->getBody()->getContents();
Log::error("微信支付 API 返回错误:", json_decode($errorBody, true));
throw new \Exception("微信支付返回错误:" . $errorBody);
}
return $resp->getBody()->getContents();
} catch (\Throwable $e) {
Log::error("微信支付订单查询失败:{$e->getMessage()}");
throw new \Exception("订单查询失败:" . $e->getMessage());
}
}
/**
* 验证微信支付签名
*/
private function verifySign($signature, $timestamp, $nonce, $body): bool
{
// 微信支付平台证书序列号(需提前获取并保存)
$wechatpaySerial = '微信支付平台证书序列号';
// 构造验签名串
$message = "{$timestamp}\n{$nonce}\n{$body}\n";
// 获取微信支付平台证书(需提前下载并保存)
$publicKey = file_get_contents(root_path() . 'cert/wechatpay_public_key.pem');
// 验证签名
$result = openssl_verify(
$message,
base64_decode($signature),
$publicKey,
'sha256WithRSAEncryption'
);
return $result === 1;
}
/**
* 解密数据
*/
private function decryptData($ciphertext, $nonce, $associatedData): ?string
{
// APIv3密钥
$apiKey = '你的APIv3密钥';
try {
$ciphertext = base64_decode($ciphertext);
if (strlen($ciphertext) <= 16) {
return null;
}
// 解密算法
$ctext = substr($ciphertext, 0, -16);
$authTag = substr($ciphertext, -16);
return openssl_decrypt(
$ctext,
'aes-256-gcm',
$apiKey,
OPENSSL_RAW_DATA,
$nonce,
$authTag,
$associatedData
);
} catch (\Exception $e) {
Log::error('解密微信支付回调数据失败: ' . $e->getMessage());
return null;
}
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "abeeceb9a3eed0f7bab8445271a50739",
"content-hash": "94ac66d0de24bd6012b59701a6bcfc94",
"packages": [
{
"name": "adbario/php-dot-notation",
......@@ -4943,6 +4943,73 @@
},
"time": "2021-02-24T11:53:01+00:00"
},
{
"name": "wechatpay/wechatpay",
"version": "1.4.12",
"source": {
"type": "git",
"url": "https://github.com/wechatpay-apiv3/wechatpay-php.git",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wechatpay-apiv3/wechatpay-php/zipball/bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-libxml": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.5 || ^7.0",
"guzzlehttp/uri-template": "^0.2 || ^1.0",
"php": ">=7.1.2"
},
"require-dev": {
"phpstan/phpstan": "^0.12.89 || ^1.0",
"phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5"
},
"bin": [
"bin/CertificateDownloader.php"
],
"type": "library",
"autoload": {
"psr-4": {
"WeChatPay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "James ZHANG",
"homepage": "https://github.com/TheNorthMemory"
},
{
"name": "WeChatPay Community",
"homepage": "https://developers.weixin.qq.com/community/pay"
}
],
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"homepage": "https://pay.weixin.qq.com/",
"keywords": [
"AES-GCM",
"aes-ecb",
"openapi-chainable",
"rsa-oaep",
"wechatpay",
"xml-builder",
"xml-parser"
],
"support": {
"issues": "https://github.com/wechatpay-apiv3/wechatpay-php/issues",
"source": "https://github.com/wechatpay-apiv3/wechatpay-php/tree/v1.4.12"
},
"time": "2025-01-26T14:16:41+00:00"
},
{
"name": "workerman/gateway-worker",
"version": "v3.0.22",
......
-----BEGIN CERTIFICATE-----
MIIEITCCAwmgAwIBAgIUETv0D7fv9MK6aRZqIRpFcOlprAEwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNTI2MDMzOTU1WhcNMzAwNTI1MDMzOTU1WjB7MRMwEQYDVQQDDAox
NzE2NTQwNjUyMRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xJzAlBgNVBAsM
HumHjeW6hua1gemAn+enkeaKgOaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04xETAP
BgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
1jC9WzCraMewjDHfCjyQcCTpv7MFSSXvntfQY2TniPyrXa3x9WNVRCF7bTMHR/jd
VAykR70SrVmY4PgNhvgCw3boFfu6qzecThy4oiuWuFdJ5jw/sJzkAW8P+pFcqZFS
h08b8QJKmExb7izEbt6JKURObzr3DxmMRU5lJoG1fZIgVa3PuI8q9qgUIAtB3JjN
NGR4XRUtDjvthAr/Li0dbLF320RJ7f2Pi5LdIlZQp/1P0+Cnt+bzV9xMxUrXxv3t
BejU/bVbBgvZBCUjUPg4lXmsz+uKl60IipH/RyVeI+17UMeIuoRcD2wEvCaJkGvz
kdCezrlKHs2PqVFf09DqvwIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0PBAQD
AgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1cy5j
b20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3
NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMzQTg3
QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBAGfr6HeuQBJfnlCD
IxTe2CfENF1Hk7A3hGlMqSAij+FaH4ccuvitXJ4Zq1bRKQFoa1wfBxPSASr0U46I
SZxO71Lk77MsqIFDxN6GUCoKRbX8ZyydMZ5R6OB1rVHJmh2BftwCEMes5RdM6gOH
1QceLU5Wraygt7icCoJJrmXIEEcT0bC/C+GNt4pxURyeMCVx8gzCCwXTTWrgXEdd
5gih1mV4olM37yXKVfJzSGLIMs+18uGyjy32C5v2VmqG8/6SJvOPcDaEWrISo2Xh
72KUGHRsAPWWT8E2GMYGcXpNqdXEWrbWOy29eI5Gl5X1EaLB5PK5zQFUMl+J9yk1
+wGxFyU=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWML1bMKtox7CM
Md8KPJBwJOm/swVJJe+e19BjZOeI/KtdrfH1Y1VEIXttMwdH+N1UDKRHvRKtWZjg
+A2G+ALDdugV+7qrN5xOHLiiK5a4V0nmPD+wnOQBbw/6kVypkVKHTxvxAkqYTFvu
LMRu3okpRE5vOvcPGYxFTmUmgbV9kiBVrc+4jyr2qBQgC0HcmM00ZHhdFS0OO+2E
Cv8uLR1ssXfbREnt/Y+Lkt0iVlCn/U/T4Ke35vNX3EzFStfG/e0F6NT9tVsGC9kE
JSNQ+DiVeazP64qXrQiKkf9HJV4j7XtQx4i6hFwPbAS8JomQa/OR0J7OuUoezY+p
UV/T0Oq/AgMBAAECggEAapfw9HvhIhEW3H5JOIfxfc3xAjTucXvOp2dRztU9oN/V
zJYvbuaTXYeoLC0T945zX0u3SfhfXiPTwEnSOZQdk/cOAzq2qFTRldIXVKWKqYzP
OyRKsfMySUBjXFiRG9Y1kx8ckbGJPAfsTDi9PUvESOQ0gIiAwWP+edNM5X/xuV5g
9hy35INkPfVvq424i5JfG3mtgs2g5/GCuzZis5h2l6Tjf8Ebs/3hhcy6yIVRN12y
2RpnEXEqkUKh4NTP+xr3DiU5eo6cr0ej025CI7s3jK+G7WTaryyfMLVH3LBTaBUV
rmBS0fqg8WhnHli6AK5JUAJHMuOyWeQHTygEZsSLKQKBgQD44ws9g+Pryx+q4/js
dt78rDJH7tL9D7tVWcMJ1/qVvIgQXpa0R/mXOGXsZmd+HuJ2tNspcyF0TNAGH49g
j3vDE72eNB+vz+QwqHYKA+esWx71vt5UyHzAtkNqjbMvsz006wC9ws3A0ooqY1pF
guE73se4cAYA8HRyUPyyfi9mywKBgQDcT9efnDp22WLdGJUhiyi5v0RySAIwE3TO
ug2TuY73zfYP1J17fIs6+Xs5I/r9mzisV+T5Ddlgyggpc+iHdRXgL8hNTvDjw/iF
zj3czsbhnJwB4qQsGYaGMC92Yz7Ey5fzzB8eJomtsN3Mcq+E/ln7dXAXTNopKug+
BwbXLhRZXQKBgQCOa229L8G25i8i3P1OYt9K+0ZyfylhAiWSu6Ct+1c7Y/0AUQAv
/ZfHftBkLF1AgG/aubdHysf0AxhuyJSFDtYlVSCGbRFMy8uqRFv3czCZIjNfMG+N
WIR6ylFdoeRNgWWe6HMuI1EV6+SASQYZDMHSPrNOyVvGIdKgr9NKWIbLbQKBgQCo
QOXZBNR1nfHuDu8d7gxNR3C7opjhJIrJsrfZwRYZ1Jb+Yg9flq8yfAQKkJsIAfqX
TI0XU+dXDxSKq/XDNb1eOL/NouM/35O2hNsj/ltPRG601eUxtNDTPIuS65qtaxuC
WrrNKPtuxiuuD0xS0nngHEFd2him5hj1/iHQRmXOFQKBgCLIKrWZ35IoqbNYVZPD
Ijy95iYSBaVsBsV5fXXoY6FsZyThXb9GMb5bnAs6zSb0txqwG/pQx+oAMIls2JKr
Jr31W9vMRnuFkAsw/CySGdLVwpz/DYrkNxrXIXNvgpxLV5CVIR9VTPeFKN5edtUy
LFCGV53MF75I0FESq0jUhOrd
-----END PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApccwlLAcFz8BxMOFAE8o
BksEHwHOeNcFG/V0t7ijy9PBNrvGfWsHYPZPXO2Q08jSAWdUeJznM9QntI0v1Oag
qNPf7LNAb3wJhL19kN4Ug53t4RPWz0XoZJIjJI4/ZnE7tlFdXzKDrCtk2jKWuOiz
6Fq+bWy578tXVHOiV5/M79sAAxQn+vbnPpWgVyUZ7yaZ5MhAAxEuHgoP7bAz+22h
Vo58FqF2UewQ/Ikg4A9DnpGhkB2DHCuGtyMsCJ5IHB9HW2XQ3erzVv8DiuExquaQ
SXnfC72+0gOrgJhLQmYNfCtDcz19vmoLKOXBQOiqR9Zt3hEPjlU7SXxfy8em/zYv
AQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIEFDCCAvygAwIBAgIUWBs/CqlY8N8QWs5MRSMjmfV2qY4wDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNTI2MDMzOTU1WhcNMzAwNTI1MDMzOTU1WjBuMRgwFgYDVQQDDA9U
ZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl
bnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGEwJDTjERMA8GA1UEBwwIU2hlblpo
ZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlh4zBSXUWlmnWgHhY
AZX1/HXqoJZs5NqODvuBClvvmLCF7tl6ht3ygoDaGOF5HzVxAv0QQdpcos5yHHKX
y/pjcMJJ6rDS59y/A5wqIIpWoRnRa3wdNHOfIKlU8AWGmqUR+zg9b6w+9wNRffuz
XKdLIGDG5zUTM0bLTJQkYBNl8HXtt8c5OtvNiZ0y68ZTNMm+M+UJbqrgVoOcKiTJ
xLNkfP+60QL1CJIEdTKjc8KX0f9uDQ+HcTzlbk3Jp+lj/dxJaHALFqn7cPgD70if
SKjvB34KmZ5gMixaM/fRyVZ2lL10q/9Jz72ycCqZvaGi3KSpVyukG650xQwMlC0u
vFMBAgMBAAGjgbkwgbYwCQYDVR0TBAIwADALBgNVHQ8EBAMCA/gwgZsGA1UdHwSB
kzCBkDCBjaCBiqCBh4aBhGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMv
aXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4
RUJEMiZzZz1IQUNDNDcxQjY1NDIyRTEyQjI3QTlEMzNBODdBRDFDREY1OTI2RTE0
MDM3MTANBgkqhkiG9w0BAQsFAAOCAQEAhEVAKCvwHRCkqbvGwsmzW6WYhK/DrDtK
9J9/RNXn4eubwjkPiiMPH3Nvvi7SToZIAVQiWDVJ12te3MPBqOZmfedaVYULE2qA
bzUE8ET10hO/QQcUN6OlbGkvrafqFjEsnDeJ4wwLTmXcd3iXHewec7JuaTFC8dFQ
h00qfNxxlrJDskfhABTirdKHHDwFDrEQIy3RcpgFKa3U0YQoQghXT2oSMI/r0RKl
9cLBMdZumlVPSr0i131tFflZ88xiN6/l3gALSIWtBUBe1x+xGqwFiMWzAap0rDdK
axkHXvHzese6Kj/0GVaF28RwUeXojugZW2Yap8epoTUOqjQrrEgGlg==
-----END CERTIFICATE-----
\ No newline at end of file
......@@ -27,48 +27,30 @@ return [
],
'wechat' => [
'default' => [
// 「必填」商户号,服务商模式下为服务商商户号
// 可在 https://pay.weixin.qq.com/ 账户中心->商户信息 查看
'mch_id' => '',
// 「选填」v2商户私钥
'mch_secret_key_v2' => '',
// 「必填」v3 商户秘钥
// 即 API v3 密钥(32字节,形如md5值),可在 账户中心->API安全 中设置
'mch_secret_key' => '',
// 「必填」商户私钥 字符串或路径
// 即 API证书 PRIVATE KEY,可在 账户中心->API安全->申请API证书 里获得
// 文件名形如:apiclient_key.pem
'mch_secret_cert' => '',
// 「必填」商户公钥证书路径
// 即 API证书 CERTIFICATE,可在 账户中心->API安全->申请API证书 里获得
// 文件名形如:apiclient_cert.pem
'mch_public_cert_path' => '',
// 「必填」微信回调url
// 不能有参数,如?号,空格等,否则会无法正确回调
'notify_url' => 'https://yansongda.cn/wechat/notify',
// 「选填」公众号 的 app_id
// 可在 mp.weixin.qq.com 设置与开发->基本配置->开发者ID(AppID) 查看
'mp_app_id' => '2016082000291234',
// 「选填」小程序 的 app_id
'mini_app_id' => '',
// 「选填」app 的 app_id
'app_id' => '',
// 「选填」服务商模式下,子公众号 的 app_id
'sub_mp_app_id' => '',
// 「选填」服务商模式下,子 app 的 app_id
'sub_app_id' => '',
// 「选填」服务商模式下,子小程序 的 app_id
'sub_mini_app_id' => '',
// 「选填」服务商模式下,子商户id
'sub_mch_id' => '',
// 「选填」(适用于 2024-11 及之前开通微信支付的老商户)微信支付平台证书序列号及证书路径,强烈建议 php-fpm 模式下配置此参数
// 「必填」微信支付公钥ID及证书路径,key 填写形如 PUB_KEY_ID_0000000000000024101100397200000006 的公钥id,见 https://pay.weixin.qq.com/doc/v3/merchant/4013053249
'wechat_public_cert_path' => [
'45F59D4DABF31918AFCEC556D5D2C6E376675D57' => __DIR__.'/Cert/wechatPublicKey.crt',
'PUB_KEY_ID_0000000000000024101100397200000006' => __DIR__.'/Cert/publickey.pem',
],
// 「选填」默认为正常模式。可选为: MODE_NORMAL, MODE_SERVICE
'mode' => \Yansongda\Pay\Pay::MODE_NORMAL,
// 商户号
'mchid' => env('WECHATPAY_MCHID', '1716540652'),
// 商户证书序列号
'serial_no' => env('WECHATPAY_SERIAL_NO', '113BF40FB7EFF4C2BA69166A211A4570E969AC01'),
// APIv3密钥
'apiv3_key' => env('WECHATPAY_APIV3_KEY', 'VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA'),
// AppID
'appid' => env('WECHATPAY_APPID', 'wx24528a99c58e1919'),
// 商户私钥文件路径
'private_key_path' =>__DIR__.'/cert/wechat/apiclient_key.pem',
// 商户证书文件路径
'certificate_path' => __DIR__.'/cert/wechat/apiclient_cert.pem',
// 通知地址
'notify_url' => env('WECHATPAY_NOTIFY_URL', 'https://api.kuajingxl.com/api/payController/wechatNotify'),
// 微信支付公钥路径
'publick_key_path' =>__DIR__.'/cert/wechat/publickey.pem',
'publick_key_id' =>'PUB_KEY_ID_0117165406522025052600322092003400',
//平台证书
'wechatpay_platform_path' => __DIR__.'/cert/wechat/wechatpay_581B3F0AA958F0DF105ACE4C45232399F576A98E.pem',
'wechatpay_platform_id' => '581B3F0AA958F0DF105ACE4C45232399F576A98E'
// 'certificate_path' => __DIR__ . '/cert/wechat/wechatpay_platform.pem', // 微信平台证书(新下载)
]
],
];
\ No newline at end of file
......@@ -14,7 +14,7 @@ use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use AlibabaCloud\Dysmsapi\Dysmsapi;
use think\facade\Cache;
use think\Facade\Db;
use think\facade\Db;
/**
* 短信发送类
......
File added
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../wechatpay/wechatpay/bin/CertificateDownloader.php)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/wechatpay/wechatpay/bin/CertificateDownloader.php');
}
}
return include __DIR__ . '/..'.'/wechatpay/wechatpay/bin/CertificateDownloader.php';
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/CertificateDownloader.php
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*
......@@ -10,10 +10,10 @@ return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'7448f3465e10b5f033e4babb31eb0b06' => $vendorDir . '/topthink/think-orm/src/helper.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => $vendorDir . '/topthink/think-orm/stubs/load_stubs.php',
'15ec93fa4ce4b2d53816a1a5f2c514e2' => $vendorDir . '/topthink/think-validate/src/helper.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'6b998e7ad3182c0d21d23780badfa07b' => $vendorDir . '/yansongda/supports/src/Functions.php',
......
......@@ -23,6 +23,7 @@ return array(
'Yansongda\\Pay\\' => array($vendorDir . '/yansongda/pay/src'),
'Yansongda\\Artful\\' => array($vendorDir . '/yansongda/artful/src'),
'Workerman\\' => array($vendorDir . '/workerman/workerman'),
'WeChatPay\\' => array($vendorDir . '/wechatpay/wechatpay/src'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
......
......@@ -11,10 +11,10 @@ class ComposerStaticInit118552ef2b3077b276422e248ece3c62
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'7448f3465e10b5f033e4babb31eb0b06' => __DIR__ . '/..' . '/topthink/think-orm/src/helper.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => __DIR__ . '/..' . '/topthink/think-orm/stubs/load_stubs.php',
'15ec93fa4ce4b2d53816a1a5f2c514e2' => __DIR__ . '/..' . '/topthink/think-validate/src/helper.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'6b998e7ad3182c0d21d23780badfa07b' => __DIR__ . '/..' . '/yansongda/supports/src/Functions.php',
......@@ -75,6 +75,7 @@ class ComposerStaticInit118552ef2b3077b276422e248ece3c62
'W' =>
array (
'Workerman\\' => 10,
'WeChatPay\\' => 10,
),
'S' =>
array (
......@@ -240,6 +241,10 @@ class ComposerStaticInit118552ef2b3077b276422e248ece3c62
array (
0 => __DIR__ . '/..' . '/workerman/workerman',
),
'WeChatPay\\' =>
array (
0 => __DIR__ . '/..' . '/wechatpay/wechatpay/src',
),
'Symfony\\Polyfill\\Php83\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
......
......@@ -5352,6 +5352,76 @@
},
"install-path": "../veitool/admin"
},
{
"name": "wechatpay/wechatpay",
"version": "1.4.12",
"version_normalized": "1.4.12.0",
"source": {
"type": "git",
"url": "https://github.com/wechatpay-apiv3/wechatpay-php.git",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wechatpay-apiv3/wechatpay-php/zipball/bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-libxml": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.5 || ^7.0",
"guzzlehttp/uri-template": "^0.2 || ^1.0",
"php": ">=7.1.2"
},
"require-dev": {
"phpstan/phpstan": "^0.12.89 || ^1.0",
"phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5"
},
"time": "2025-01-26T14:16:41+00:00",
"bin": [
"bin/CertificateDownloader.php"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"WeChatPay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "James ZHANG",
"homepage": "https://github.com/TheNorthMemory"
},
{
"name": "WeChatPay Community",
"homepage": "https://developers.weixin.qq.com/community/pay"
}
],
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"homepage": "https://pay.weixin.qq.com/",
"keywords": [
"AES-GCM",
"aes-ecb",
"openapi-chainable",
"rsa-oaep",
"wechatpay",
"xml-builder",
"xml-parser"
],
"support": {
"issues": "https://github.com/wechatpay-apiv3/wechatpay-php/issues",
"source": "https://github.com/wechatpay-apiv3/wechatpay-php/tree/v1.4.12"
},
"install-path": "../wechatpay/wechatpay"
},
{
"name": "workerman/gateway-worker",
"version": "v3.0.22",
......
......@@ -1750,6 +1750,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'wechatpay/wechatpay' => array(
'pretty_version' => '1.4.12',
'version' => '1.4.12.0',
'reference' => 'bd2148e0456f560df4d1c857d6cd1f8ad9f5222e',
'type' => 'library',
'install_path' => __DIR__ . '/../wechatpay/wechatpay',
'aliases' => array(),
'dev_requirement' => false,
),
'workerman/gateway-worker' => array(
'pretty_version' => 'v3.0.22',
'version' => '3.0.22.0',
......
<?php
// This file is automatically generated at:2025-05-23 17:01:16
// This file is automatically generated at:2025-05-26 14:08:29
declare (strict_types = 1);
return array (
0 => 'think\\captcha\\CaptchaService',
......
......@@ -53,12 +53,36 @@ class QRcode
* @param int $size 图片大小
* @return Obj $this
*/
public function png($url,$outfile=false,$evel='H',$size=5){
$this->outfile = $outfile ?: $this->cache_dir.'/'.time().'.png';
\QRcode::png($url,$this->outfile,$evel,$size,2);
public function png($url, $outfile = null, $level = 'H', $size = 5) {
// 使用系统临时目录
$tempDir = sys_get_temp_dir() . '/qrcodes/';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0777, true);
}
$this->outfile = $tempDir . uniqid('wxpay_') . '.png';
// 生成二维码
\QRcode::png($url, $this->outfile, $level, $size, 2);
// 验证文件
if (!file_exists($this->outfile)) {
throw new Exception("文件生成失败,路径:{$this->outfile}");
}
return $this;
}
public function getBase64() {
if (!file_exists($this->outfile)) {
throw new Exception('请先调用png()方法生成二维码');
}
$imageData = file_get_contents($this->outfile);
unlink($this->outfile); // 清理临时文件
return 'data:image/png;base64,' . base64_encode($imageData);
}
/**
* 添加logo到二维码中
* @param $logo
......
# 变更历史
## [1.4.12](../../compare/v1.4.11...v1.4.12) - 2025-01-27
- 修正`APIv2`特殊`GET`请求抛异常问题,相关记录见[这里](https://github.com/wechatpay-apiv3/wechatpay-php/issues/146)
## [1.4.11](../../compare/v1.4.10...v1.4.11) - 2024-12-27
-`APIv2`服务端返回值做精细判断,对于`return_code`(返回状态码)及/或`result_code`(业务结果)有key且值不为`SUCCESS`的情形,抛出客户端`RejectionException`异常,并加入[AuthcodetoopenidTest.php](./tests/OpenAPI/V2/Tools/AuthcodetoopenidTest.php)异常处理示例。
## [1.4.10](../../compare/v1.4.9...v1.4.10) - 2024-09-19
- 客户端在`RSA`非对称加解密方案上,不再支持`OPENSSL_PKCS1_PADDING`填充模式,相关记录见[这里](https://github.com/wechatpay-apiv3/wechatpay-php/issues/133)
- 增加[`#[\SensitiveParameter]`](https://www.php.net/manual/zh/class.sensitiveparameter.php)参数注解,加强信息安全;
- 支持PHP8.4运行时;
## [1.4.9](../../compare/v1.4.8...v1.4.9) - 2023-11-21
- 支持PHP8.3运行时
## [1.4.8](../../compare/v1.4.7...v1.4.8) - 2023-01-05
- 新增海外账单下载`/v3/global/statements`应答特殊处理逻辑;
## [1.4.7](../../compare/v1.4.6...v1.4.7) - 2022-12-06
- 对PHP8.2的官方支持,如下PHP8.2的特性需要被提及:
- ext-openssl 有若干调整,已知在 `OpenSSL3.0` 上,常量 `RSA_SSLV23_PADDING` 被删除(详细可阅读 openssl/openssl#14216, openssl/openssl#14283),PHP做了兼容处理,如果扩展依赖的是`OpenSSL3.0`,则对应的`OPENSSL_SSLV23_PADDING`常量将不存在,进而影响到了「非对称加解密混合填充模式的测试用例」的覆盖(详情可阅读 shivammathur/setup-php#658)。本类库并不支持此填充模式,删除对`OPENSSL_SSLV23_PADDING`的测试断言,向前兼容;
- 对象动态属性的废弃提示([Deprecate dynamic properties](https://wiki.php.net/rfc/deprecate_dynamic_properties)),本类库实例构造的是`ArrayIterator`的一个“伪”动态属性结构体,对象属性访问实则访问的是`ArrayObject`内置`__storage`属性,形似动态属性实则不是;此废弃提示对本类库本身无影响;
## [1.4.6](../../compare/v1.4.5...v1.4.6) - 2022-08-19
- 取消 `APIv2` 上的`trigger_error`提醒,以消除不必要的恐慌;
- 优化 `Transformer::walk` 方法,以支持 [Stringable](https://www.php.net/manual/zh/class.stringable.php) 对象的值转换;
## [1.4.5](../../compare/v1.4.4...v1.4.5) - 2022-05-21
- 新增`APIv3`请求/响应特殊验签逻辑,国内两个下载接口自动忽略验签,海外商户账单下载仅验RSA签名,详见 [#94](https://github.com/wechatpay-apiv3/wechatpay-php/issues/94);
- 新增`APIv3`[海外商户账单下载](https://pay.weixin.qq.com/wiki/doc/api/wxpay/ch/fusion_wallet_ch/QuickPay/chapter8_5.shtml)测试用例,示例说明如何验证流`SHA1`摘要;
## [1.4.4](../../compare/v1.4.3...v1.4.4) - 2022-05-19
- 新增`APIv3`[客诉图片下载](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter10_2_18.shtml)测试用例,示例说明如何避免[double pct-encoded](https://github.com/guzzle/uri-template/issues/18)问题;
- PHP内置函数`hash`方法在`PHP8`变更了返回值逻辑,代之为抛送`ValueError`异常,优化`MediaUtilTest`测试用例,以兼容`PHP7`;
- 新增`APIv2`请求/响应白名单`URL`及调整验签逻辑,对于白名单内的请求,已知无`sign`返回,应用侧自动忽略验签,详见 [#92](https://github.com/wechatpay-apiv3/wechatpay-php/issues/92);
## [1.4.3](../../compare/v1.4.2...v1.4.3) - 2022-01-04
- 优化,严格限定初始化时`mchid`为字符串;
- 优化,严格限定`chain`接口函数入参为字符串;
- 优化`README`,增加`常见问题`示例说明`URL template`用法;
## [1.4.2](../../compare/v1.4.1...v1.4.2) - 2021-12-02
- 优化`Rsa::parse`代码逻辑,去除`is_resource`/`is_object`检测;
- 调整`Rsa::from[Pkcs8|Pkcs1|Spki]`加载语法糖实现,以`Rsa::from`为统一入口;
- 优化`ClientDecorator::request[Async]`处理逻辑,优先替换`URI Template`变量,可支持短链模式调用接口;
## [1.4.1](../../compare/v1.4.0...v1.4.1) - 2021-11-03
- 新增`phpstan/phpstan:^1.0`支持;
- 优化代码,消除函数内部不安全的`Unsafe call to private method|property ... through static::`调用隐患;
## [1.4.0](../../compare/v1.3.2...v1.4.0) - 2021-10-24
- 调整`Guzzle`最低版本支持至v6.5.0,相应降低PHP版本要求至7.1.2,相关见[#71519](http://bugs.php.net/71519);
- 调整`PHPUnit`最低版本至v7.5.0||v8.5.16||v9.3.5,相关问题见[#4663](https://github.com/sebastianbergmann/phpunit/issues/4663);
详细说明可见[1.3至1.4升级指南](UPGRADING.md)
## [1.3.2](../../compare/v1.3.1...v1.3.2) - 2021-09-30
- 增加`MediaUtil::setMeta`函数,以支持特殊场景(API)下`meta`数据结构的特殊需求;
## [1.3.1](../../compare/v1.3.0...v1.3.1) - 2021-09-22
- 修正`APIv2`上,合单支付产品`xml`入参是`combine_mch_id`引发的不适问题;
## [1.3.0](../../compare/v1.2.2...v1.3.0) - 2021-09-18
- 增加IDE提示`OpenAPI\V2`&`OpenAPI\V3`的两个入口,接口描述文件拆分为单独的包发行,生产环境无需安装(没必要),仅面向开发环境;
- 优化`userAgent`方法,使拼接`User-Agent`字典清晰可读;
- 优化`README`,增加`V3`通知验签注释说明,增加`v2`链式`otherwise`处理逻辑说明;
## [1.2.2](../../compare/v1.2.1...v1.2.2) - 2021-09-09
- 以`at sign`形式,温和提示`APIv2`的`DEP_XML_PROTOCOL_IS_REACHABLE_EOL`,相关[#38](https://github.com/wechatpay-apiv3/wechatpay-php/issues/38);
- 优化`Transformer::toArray`函数,对入参`xml`非法时,返回空`array`,并把最后一条错误信息温和地打入`E_USER_NOTICE`通道;
- 修正`Formatter::ksort`排列键值时兼容问题,使用`字典序(dictionary order)`排序,相关[#41](https://github.com/wechatpay-apiv3/wechatpay-php/issues/41), 感谢 @suiaiyun 报告此问题;
## [1.2.1](../../compare/v1.2.0...v1.2.1) - 2021-09-06
- 增加`加密RSA私钥`的测试用例覆盖;
- 优化文档样例及升级指南,修正错别字;
- 优化内部`withDefaults`函数,使用变长参数合并初始化参数;
- 优化`Rsa::encrypt`及`Rsa::decrpt`方法,增加第三可选参数,以支持`OPENSSL_PKCS1_PADDING`填充模式的加解密;
## [1.2.0](../../compare/v1.1.4...v1.2.0) - 2021-09-02
- 新增`Rsa::from`统一加载函数,以接替`PemUtil::loadPrivateKey`函数功能;
- 新增`Rsa::fromPkcs1`, `Rsa::fromPkcs8`, `Rsa::fromSpki`语法糖,以支持从云端加载RSA公/私钥;
- 新增RSA公钥`Rsa::pkcs1ToSpki`格式转换函数,入参是`base64`字符串;
- 标记 `PemUtil::loadPrivateKey`及`PemUtil::loadPrivateKeyFromString`为`不推荐用法`;
- 详细变化可见[1.1至1.2升级指南](UPGRADING.md)
## [1.1.4](../../compare/v1.1.3...v1.1.4) - 2021-08-26
- 优化`平台证书下载工具`使用说明,增加`composer exec`执行方法说明;
- 优化了一点点代码结构,使逻辑更清晰了一些;
## [1.1.3](../../compare/v1.1.2...v1.1.3) - 2021-08-22
- 优化`README`,增加`回调通知`处理说明及样本代码;
- 优化测试用例,使用`严格限定名称`方式引用系统内置函数;
- 优化`Makefile`,在生成模拟证书时,避免产生`0x00`开头的证书序列号;
- 调整`composer.json`,新增`guzzlehttp/uri-template:^1.0`支持;
## [1.1.2](../../compare/V1.1.1...v1.1.2) - 2021-08-19
- 优化`README`,`密钥`、`证书`等相关术语保持一致;
- 优化`UPGRADING`,增加从`php_sdk_v3.0.10`迁移指南;
- 优化测试用例,完整覆盖`PHP7.2/7.3/7.4/8.0 + Linux/macOS/Windows`运行时;
- 调整`composer.json`,去除`test`, `phpstan`命令,面向生产环境可用;
## [1.1.1](../../compare/v1.1.0...V1.1.1) - 2021-08-13
- 优化内部中间件始终从`\GuzzleHttp\Psr7\Stream::__toString`取值,并在取值后,判断如果影响了`Stream`指针,则回滚至开始位;
- 增加`APIv2`上一些特殊用法示例,增加`数据签名`样例;
- 增加`APIv2`文档提示说明`DEP_XML_PROTOCOL_IS_REACHABLE_EOL`;
- 修正`APIv2`上,转账至用户零钱接口,`xml`入参是`mchid`引发的不适问题;
- 增加`APIv2`上转账至用户零钱接口测试用例,样例说明如何进行异常捕获;
## [1.1.0](../../compare/v1.0.9...v1.1.0) - 2021-08-07
- 调整内部中间件栈顺序,并对`APIv3`的正常返回内容(`20X`)做精细判断,逻辑异常时使用`\GuzzleHttp\Exception\RequestException`抛出,应用端可捕获源返回内容;
- 对于`30X`及`4XX`,`5XX`返回,`Guzzle`基础中间件默认已处理,具体用法及使用,可参考`\GuzzleHttp\RedirectMiddleware`及`\GuzzleHttp\Middleware::httpErrors`说明;
- 详细变化可见[1.0至1.1升级指南](UPGRADING.md)
## [1.0.9](../../compare/v1.0.8...v1.0.9) - 2021-08-05
- 优化平台证书下载器`CertificateDownloader`异常处理逻辑部分,详见[#22](https://github.com/wechatpay-apiv3/wechatpay-php/issues/22);
- 优化`README`使用示例的异常处理部分;
## [1.0.8](../../compare/v1.0.7...v1.0.8) - 2021-07-26
- 增加`WeChatPay\Crypto\Hash::equals`方法,用于比较`APIv2`哈希签名值是否相等;
- 建议使用`APIv2`的商户,在回调通知场景中,使用此方法来验签,相关说明见PHP[hash_equals](https://www.php.net/manual/zh/function.hash-equals.php)说明;
## [1.0.7](../../compare/v1.0.6...v1.0.7) - 2021-07-22
- 完善`APIv3`及`APIv2`工厂方法初始化说明,推荐优先使用`APIv3`;
## [1.0.6](../../compare/v1.0.5...v1.0.6) - 2021-07-21
- 调整 `Formatter::nonce` 算法,使用密码学安全的`random_bytes`生产`BASE62`随机字符串;
## [1.0.5](../../compare/v1.0.4...v1.0.5) - 2021-07-08
- 核心代码全部转入严格类型`declare(strict_types=1)`校验模式;
- 调整 `Authorization` 头格式顺序,debug时优先展示关键信息;
- 调整 媒体文件`MediaUtil`类读取文件时,严格二进制读,避免跨平台干扰问题;
- 增加 测试用例覆盖`APIv2`版用法;
## [1.0.4](../../compare/v1.0.3...v1.0.4) - 2021-07-05
- 修正 `segments` 首字符大写时异常问题;
- 调整 初始入参如果有提供`handler`,透传给了下游客户端问题;
- 增加 `PHP`最低版本说明,相关问题 [#10](https://github.com/wechatpay-apiv3/wechatpay-php/issues/10);
- 增加 测试用例已基本全覆盖`APIv3`版用法;
## [1.0.3](../../compare/v1.0.2...v1.0.3) - 2021-06-28
- 初始化`jsonBased`入参判断,`平台证书及序列号`结构体内不能含`商户序列号`,相关问题 [#8](https://github.com/wechatpay-apiv3/wechatpay-php/issues/8);
- 修复文档错误,相关 [#7](https://github.com/wechatpay-apiv3/wechatpay-php/issues/7);
- 优化 `github actions`,针对PHP7.2单独缓存依赖(`PHP7.2`下只能跑`PHPUnit8`,`PHP7.3`以上均可跑`PHPUnit9`);
- 增加 `composer test` 命令并集成进 `CI` 内(测试用例持续增加中);
- 修复 `PHPStan` 所有遗留问题;
## [1.0.2](../../compare/v1.0.1...v1.0.2) - 2021-06-24
- 优化了一些性能;
- 增加 `github actions` 覆盖 PHP7.2/7.3/7.4/8.0 + Linux/macOS/Windows环境;
- 提升 `phpstan` 至 `level8` 最严谨级别,并修复大量遗留问题;
- 优化 `\WeChatPay\Exception\WeChatPayException` 异常类接口;
- 完善文档及平台证书下载器用法说明;
## [1.0.1](../../compare/v1.0.0...v1.0.1) - 2021-06-21
- 优化了一些性能;
- 修复了大量 `phpstan level6` 静态分析遗留问题;
- 新增`\WeChatPay\Exception\WeChatPayException`异常类接口;
- 完善文档及方法类型签名;
## [1.0.0](../../compare/6782ac3..v1.0.0) - 2021-06-18
源自 `wechatpay-guzzle-middleware`,不兼容源版,顾自 `v1.0.0` 开始。
- `APIv2` & `APIv3` 同质化调用SDK,默认为 `APIv3` 版;
- 标记 `APIv2` 为不推荐调用,预期 `v2.0` 会移除掉;
- 支持 `同步(sync)`(默认)及 `异步(async)` 请求服务端接口;
- 支持 `链式(chain)` 请求服务端接口;
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
# 微信支付 WeChatPay OpenAPI SDK
[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP
[![GitHub actions](https://github.com/wechatpay-apiv3/wechatpay-php/workflows/CI/badge.svg)](https://github.com/wechatpay-apiv3/wechatpay-php/actions)
[![Packagist Stars](https://img.shields.io/packagist/stars/wechatpay/wechatpay)](https://packagist.org/packages/wechatpay/wechatpay)
[![Packagist Downloads](https://img.shields.io/packagist/dm/wechatpay/wechatpay)](https://packagist.org/packages/wechatpay/wechatpay)
[![Packagist Version](https://img.shields.io/packagist/v/wechatpay/wechatpay)](https://packagist.org/packages/wechatpay/wechatpay)
[![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/wechatpay/wechatpay)](https://packagist.org/packages/wechatpay/wechatpay)
[![Packagist License](https://img.shields.io/packagist/l/wechatpay/wechatpay)](https://packagist.org/packages/wechatpay/wechatpay)
## 概览
基于 [Guzzle HTTP Client](http://docs.guzzlephp.org/) 的微信支付 PHP 开发库。
### 功能介绍
1. 微信支付 APIv2 和 APIv3 的 Guzzle HTTP 客户端,支持 [同步](#同步请求)[异步](#异步请求) 发送请求,并自动进行请求签名和应答验签
1. [链式实现的 URI Template](#链式-uri-template)
1. [敏感信息加解密](#敏感信息加解密)
1. [回调通知](#回调通知)的验签和解密
## 项目状态
当前版本为 `1.4.12` 版。
项目版本遵循 [语义化版本号](https://semver.org/lang/zh-CN/)
如果你使用的版本 `<=v1.3.2`,升级前请参考 [升级指南](UPGRADING.md)
## 环境要求
项目支持的环境如下:
+ Guzzle 7.0,PHP >= 7.2.5
+ Guzzle 6.5,PHP >= 7.1.2
我们推荐使用目前处于 [Active Support](https://www.php.net/supported-versions.php) 阶段的 PHP 8 和 Guzzle 7。
## 安装
推荐使用 PHP 包管理工具 [Composer](https://getcomposer.org/) 安装 SDK:
```shell
composer require wechatpay/wechatpay
```
## 开始
:information_source: 以下是 [微信支付 API v3](https://pay.weixin.qq.com/docs/merchant/development/interface-rules/introduction.html) 的指引。如果你是 API v2 的使用者,请看 [README_APIv2](README_APIv2.md)
### 概念
+ **商户 API 证书**,是用来证实商户身份的。证书中包含商户号、证书序列号、证书有效期等信息,由证书授权机构(Certificate Authority ,简称 CA)签发,以防证书被伪造或篡改。详情见 [什么是商户API证书?如何获取商户API证书?](https://kf.qq.com/faq/161222NneAJf161222U7fARv.html)
+ **商户 API 私钥**。你申请商户 API 证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。为了证明 API 请求是由你发送的,你应使用商户 API 私钥对请求进行签名。
> :key: 不要把私钥文件暴露在公共场合,如上传到 Github,写在 App 代码中等。
+ **微信支付平台证书**。微信支付平台证书是指:由微信支付负责申请,包含微信支付平台标识、公钥信息的证书。你需使用微信支付平台证书中的公钥验证 API 应答和回调通知的签名。
> :bookmark: 通用的 composer 命令,像安装依赖包一样 [下载平台证书](#如何下载平台证书) 文件,供SDK初始化使用。
+ **证书序列号**。每个证书都有一个由 CA 颁发的唯一编号,即证书序列号。
+ **微信支付公钥**,用于应答及回调通知的数据签名,可在 [微信支付商户平台](https://pay.weixin.qq.com) -> 账户中心 -> API安全 直接下载。
+ **微信支付公钥ID**,是微信支付公钥的唯一标识,可在 [微信支付商户平台](https://pay.weixin.qq.com) -> 账户中心 -> API安全 直接查看。
### 初始化一个APIv3客户端
```php
<?php
require_once('vendor/autoload.php');
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
// 设置参数
// 商户号
$merchantId = '190000****';
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
$merchantPrivateKeyFilePath = 'file:///path/to/merchant/apiclient_key.pem';
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
// 「商户API证书」的「证书序列号」
$merchantCertificateSerial = '3775B6A45ACD588826D15E583A95F5DD********';
// 从本地文件中加载「微信支付平台证书」,可由内置CLI工具下载到,用来验证微信支付应答的签名
$platformCertificateFilePath = 'file:///path/to/wechatpay/certificate.pem';
$onePlatformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 「微信支付平台证书」的「平台证书序列号」
// 可以从「微信支付平台证书」文件解析,也可以在 商户平台 -> 账户中心 -> API安全 查询到
$platformCertificateSerial = '7132D72A03E93CDDF8C03BBD1F37EEDF********';
// 从本地文件中加载「微信支付公钥」,用来验证微信支付应答的签名
$platformPublicKeyFilePath = 'file:///path/to/wechatpay/publickey.pem';
$twoPlatformPublicKeyInstance = Rsa::from($platformPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC);
// 「微信支付公钥」的「微信支付公钥ID」
// 需要在 商户平台 -> 账户中心 -> API安全 查询
$platformPublicKeyId = 'PUB_KEY_ID_01142321349124100000000000********';
// 构造一个 APIv3 客户端实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => $merchantCertificateSerial,
'privateKey' => $merchantPrivateKeyInstance,
'certs' => [
$platformCertificateSerial => $onePlatformPublicKeyInstance,
$platformPublicKeyId => $twoPlatformPublicKeyInstance,
],
]);
```
### 示例,第一个请求:查询「微信支付平台证书」
```php
// 发送请求
try {
$resp = $instance->chain('v3/certificates')->get(
/** @see https://docs.guzzlephp.org/en/stable/request-options.html#debug */
// ['debug' => true] // 调试模式
);
echo (string) $resp->getBody(), PHP_EOL;
} catch(\Exception $e) {
// 进行异常捕获并进行错误判断处理
echo $e->getMessage(), PHP_EOL;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
echo (string) $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
echo $e->getTraceAsString(), PHP_EOL;
}
```
当程序进入「异常捕获」逻辑,输出形如:
```json
{
"code": "RESOURCE_NOT_EXISTS",
"message": "无可用的平台证书,请在商户平台-API安全申请使用微信支付公钥。可查看指引https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/wxp-pub-key-guide.html"
}
```
即表示商户仅能运行在「微信支付公钥」模式,初始化即无需读取及配置`$platformCertificateSerial``$onePlatformPublicKeyInstance`等信息。
## 文档
### 同步请求
使用客户端提供的 `get``put``post``patch``delete` 方法发送同步请求。以 [Native支付下单](https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html) 为例。
```php
try {
$resp = $instance
->chain('v3/pay/transactions/native')
->post(['json' => [
'mchid' => '1900006XXX',
'out_trade_no' => 'native12177525012014070332333',
'appid' => 'wxdace645e0bc2cXXX',
'description' => 'Image形象店-深圳腾大-QQ公仔',
'notify_url' => 'https://weixin.qq.com/',
'amount' => [
'total' => 1,
'currency' => 'CNY'
],
]]);
echo $resp->getStatusCode(), PHP_EOL;
echo (string) $resp->getBody(), PHP_EOL;
} catch (\Exception $e) {
// 进行错误处理
echo $e->getMessage(), PHP_EOL;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
echo (string) $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
echo $e->getTraceAsString(), PHP_EOL;
}
```
请求成功后,你会获得一个 `GuzzleHttp\Psr7\Response` 的应答对象。
阅读 Guzzle 文档 [Using Response](https://docs.guzzlephp.org/en/stable/quickstart.html#using-responses) 进一步了解如何访问应答内的信息。
### 异步请求
使用客户端提供的 `getAsync``putAsync``postAsync``patchAsync``deleteAsync` 方法发送异步请求。以 [退款申请](https://pay.weixin.qq.com/docs/merchant/apis/native-payment/create.html) 为例。
```php
$promise = $instance
->chain('v3/refund/domestic/refunds')
->postAsync([
'json' => [
'transaction_id' => '1217752501201407033233368018',
'out_refund_no' => '1217752501201407033233368018',
'amount' => [
'refund' => 888,
'total' => 888,
'currency' => 'CNY',
],
],
])
->then(static function($response) {
// 正常逻辑回调处理
echo (string) $response->getBody(), PHP_EOL;
return $response;
})
->otherwise(static function($e) {
// 异常错误处理
echo $e->getMessage(), PHP_EOL;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
echo (string) $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
echo $e->getTraceAsString(), PHP_EOL;
});
// 同步等待
$promise->wait();
```
`[get|post|put|patch|delete]Async` 返回的是 [Guzzle Promises](https://github.com/guzzle/promises)。你可以做两件事:
+ 成功时使用 `then()` 处理得到的 `Psr\Http\Message\ResponseInterface`,(可选地)将它传给下一个 `then()`
+ 失败时使用 `otherwise()` 处理异常
最后使用 `wait()` 等待请求执行完成。
### 同步还是异步
对于大部分开发者,我们建议使用同步的模式,因为它更加易于理解。
如果你是具有异步编程基础的开发者,在某些连续调用 API 的场景,将多个操作通过 `then()` 流式串联起来会是一种优雅的实现方式。例如 [以函数链的形式流式下载交易帐单](https://developers.weixin.qq.com/community/pay/article/doc/000ec4521086b85fb81d6472a51013)
## 链式 URI Template
[URI Template](https://www.rfc-editor.org/rfc/rfc6570.html) 是表达 URI 中变量的一种方式。微信支付 API 使用这种方式表示 URL Path 中的单号或者 ID。
```
# 使用微信支付订单号查询订单
GET /v3/pay/transactions/id/{transaction_id}
# 使用商户订单号查询订单
GET /v3/pay/transactions/out-trade-no/{out_trade_no}
```
使用 [链式](https://en.wikipedia.org/wiki/Method_chaining) URI Template,你能像书写代码一样流畅地书写 URL,轻松地输入路径并传递 URL 参数。配置接口描述包后还能开启 [IDE提示](https://github.com/TheNorthMemory/wechatpay-openapi)
链式串联的基本单元是 URI Path 中的 [segments](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3)`segments` 之间以 `->` 连接。连接的规则如下:
+ 普通 segment
+ 直接书写。例如 `v3->pay->transactions->native`
+ 使用 `chain()`。例如 `chain('v3/pay/transactions/native')`
+ 包含连字号(-)的 segment
+ 使用驼峰 camelCase 风格书写。例如 `merchant-service` 可写成 `merchantService`
+ 使用 `{'foo-bar'}` 方式书写。例如 `{'merchant-service'}`
+ Path 变量。URL 中的 Path 变量应使用这种写法,避免自行组装或者使用 `chain()`,导致大小写处理错误
+ **推荐使用** `_variable_name_` 方式书写,支持 IDE 提示。例如 `v3->pay->transactions->id->_transaction_id_`
+ 使用 `{'{variable_name}'}` 方式书写。例如 `v3->pay->transactions->id->{'{transaction_id}'}`
+ 请求的 `HTTP METHOD` 作为链式最后的执行方法。例如 `v3->pay->transactions->native->post([ ... ])`
+ Path 变量的值,以同名参数传入执行方法
+ Query 参数,以名为 `query` 的参数传入执行方法
[查询订单](https://pay.weixin.qq.com/docs/merchant/apis/native-payment/query-by-wx-trade-no.html) `GET` 方法为例:
```php
$promise = $instance
->v3->pay->transactions->id->_transaction_id_
->getAsync([
// Query 参数
'query' => ['mchid' => '1230000109'],
// 变量名 => 变量值
'transaction_id' => '1217752501201407033233368018',
]);
```
[关闭订单](https://pay.weixin.qq.com/docs/merchant/apis/native-payment/close-order.html) `POST` 方法为例:
```php
$promise = $instance
->v3->pay->transactions->outTradeNo->_out_trade_no_->close
->postAsync([
// 请求消息
'json' => ['mchid' => '1230000109'],
// 变量名 => 变量值
'out_trade_no' => '1217752501201407033233368018',
]);
```
## 更多例子
### [视频文件上传](https://pay.weixin.qq.com/docs/partner/apis/contracted-merchant-application/video-upload.html)
```php
// 参考上述指引说明,并引入 `MediaUtil` 正常初始化,无额外条件
use WeChatPay\Util\MediaUtil;
// 实例化一个媒体文件流,注意文件后缀名需符合接口要求
$media = new MediaUtil('/your/file/path/video.mp4');
$resp = $instance-
>chain('v3/merchant/media/video_upload')
->post([
'body' => $media->getStream(),
'headers' => [
'content-type' => $media->getContentType(),
]
]);
```
### [营销图片上传](https://pay.weixin.qq.com/docs/partner/apis/cash-coupons/upload-image.html)
```php
use WeChatPay\Util\MediaUtil;
$media = new MediaUtil('/your/file/path/image.jpg');
$resp = $instance
->v3->marketing->favor->media->imageUpload
->post([
'body' => $media->getStream(),
'headers' => [
'Content-Type' => $media->getContentType(),
]
]);
```
## 敏感信息加/解密
为了保证通信过程中敏感信息字段(如用户的住址、银行卡号、手机号码等)的机密性,
+ 微信支付要求加密上送的敏感信息
+ 微信支付会加密下行的敏感信息
下面以 [特约商户进件](https://pay.weixin.qq.com/docs/partner/apis/contracted-merchant-application/applyment/submit.html) 为例,演示如何进行 [敏感信息加解密](https://pay.weixin.qq.com/docs/partner/development/interface-rules/sensitive-data-encryption.html)
```php
use WeChatPay\Crypto\Rsa;
// 做一个匿名方法,供后续方便使用,$platformPublicKeyInstance 见初始化章节
$encryptor = static function(string $msg) use ($platformPublicKeyInstance): string {
return Rsa::encrypt($msg, $platformPublicKeyInstance);
};
$resp = $instance
->chain('v3/applyment4sub/applyment/')
->post([
'json' => [
'business_code' => 'APL_98761234',
'contact_info' => [
'contact_name' => $encryptor('张三'),
'contact_id_number' => $encryptor('110102YYMMDD888X'),
'mobile_phone' => $encryptor('13000000000'),
'contact_email' => $encryptor('abc123@example.com'),
],
//...
],
'headers' => [
// $platformCertificateSerialOrPublicKeyId 见初始化章节
'Wechatpay-Serial' => $platformCertificateSerialOrPublicKeyId,
],
]);
```
## 签名
你可以使用 `Rsa::sign()` 计算调起支付时所需参数签名。以 [JSAPI支付](https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/jsapi-transfer-payment.html) 为例。
```php
use WeChatPay\Formatter;
use WeChatPay\Crypto\Rsa;
$merchantPrivateKeyFilePath = 'file:///path/to/merchant/apiclient_key.pem';
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);
$params = [
'appId' => 'wx8888888888888888',
'timeStamp' => (string)Formatter::timestamp(),
'nonceStr' => Formatter::nonce(),
'package' => 'prepay_id=wx201410272009395522657a690389285100',
];
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];
echo json_encode($params);
```
## 回调通知
回调通知受限于开发者/商户所使用的`WebServer`有很大差异,这里只给出开发指导步骤,供参考实现。
1. 从请求头部`Headers`,拿到`Wechatpay-Signature``Wechatpay-Nonce``Wechatpay-Timestamp``Wechatpay-Serial``Request-ID`,商户侧`Web`解决方案可能有差异,请求头可能大小写不敏感,请根据自身应用来定;
2. 获取请求`body`体的`JSON`纯文本;
3. 检查通知消息头标记的`Wechatpay-Timestamp`偏移量是否在5分钟之内;
4. 调用`SDK`内置方法,[构造验签名串](https://pay.weixin.qq.com/docs/merchant/development/verify-signature-overview/overview-signature-and-verification.html) 然后经`Rsa::verfify`验签;
5. 消息体需要解密的,调用`SDK`内置方法解密;
6. 如遇到问题,请拿`Request-ID`点击[这里](https://support.pay.weixin.qq.com/online-service?utm_source=github&utm_medium=wechatpay-php&utm_content=apiv3),联系官方在线技术支持;
样例代码如下:
```php
use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Formatter;
$inWechatpaySignature = '';// 请根据实际情况获取
$inWechatpayTimestamp = '';// 请根据实际情况获取
$inWechatpaySerial = '';// 请根据实际情况获取
$inWechatpayNonce = '';// 请根据实际情况获取
$inBody = '';// 请根据实际情况获取,例如: file_get_contents('php://input');
$apiv3Key = '';// 在商户平台上设置的APIv3密钥
// 根据通知的平台证书序列号,查询本地平台证书文件,
// 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
$platformPublicKeyInstance = Rsa::from('file:///path/to/wechatpay/inWechatpaySerial.pem', Rsa::KEY_TYPE_PUBLIC);
// 检查通知时间偏移量,允许5分钟之内的偏移
$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
$verifiedStatus = Rsa::verify(
// 构造验签名串
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature,
$platformPublicKeyInstance
);
if ($timeOffsetStatus && $verifiedStatus) {
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($inBody, true);
// 使用PHP7的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $aad
]] = $inBodyArray;
// 加密文本消息解密
$inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
// 把解密后的文本转换为PHP Array数组
$inBodyResourceArray = (array)json_decode($inBodyResource, true);
// print_r($inBodyResourceArray);// 打印解密后的结果
}
```
## 异常处理
`Guzzle` 默认已提供基础中间件`\GuzzleHttp\Middleware::httpErrors`来处理异常,文档可见[这里](https://docs.guzzlephp.org/en/stable/quickstart.html#exceptions)
本SDK自`v1.1`对异常处理做了微调,各场景抛送出的异常如下:
+ `HTTP`网络错误,如网络连接超时、DNS解析失败等,送出`\GuzzleHttp\Exception\RequestException`
+ 服务器端返回了 `5xx HTTP` 状态码,送出`\GuzzleHttp\Exception\ServerException`;
+ 服务器端返回了 `4xx HTTP` 状态码,送出`\GuzzleHttp\Exception\ClientException`;
+ 服务器端返回了 `30x HTTP` 状态码,如超出SDK客户端重定向设置阈值,送出`\GuzzleHttp\Exception\TooManyRedirectsException`;
+ 服务器端返回了 `20x HTTP` 状态码,如SDK客户端逻辑处理失败,例如应答签名验证失败,送出`\GuzzleHttp\Exception\RequestException`
+ 请求签名准备阶段,`HTTP`请求未发生之前,如PHP环境异常、商户私钥异常等,送出`\UnexpectedValueException`;
+ 初始化时,如把`商户证书序列号`配置成`平台证书序列号`,送出`\InvalidArgumentException`;
以上示例代码,均含有`catch``otherwise`错误处理场景示例,测试用例也覆盖了[5xx/4xx/20x异常](tests/ClientDecoratorTest.php),开发者可参考这些代码逻辑进行错误处理。
## 定制
当默认的本地签名和验签方式不适合你的系统时,你可以通过实现`signer`或者`verifier`中间件来定制签名和验签,比如,你的系统把商户私钥集中存储,业务系统需通过远程调用进行签名。
以下示例用来演示如何替换SDK内置中间件,来实现远程`请求签名``结果验签`,供商户参考实现。
<details>
<summary>例:内网集中签名/验签解决方案</summary>
```php
use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
// 假设集中管理服务器接入点为内网`http://192.168.169.170:8080/`地址,并提供两个URI供签名及验签
// - `/wechatpay-merchant-request-signature` 为请求签名
// - `/wechatpay-response-merchant-validation` 为响应验签
$client = new Client(['base_uri' => 'http://192.168.169.170:8080/']);
// 请求参数签名,返回字符串形如`\WeChatPay\Formatter::authorization`返回的字符串
$remoteSigner = function (RequestInterface $request) use ($client, $merchantId): string {
return (string)$client->post('/wechatpay-merchant-request-signature', ['json' => [
'mchid' => $merchantId,
'verb' => $request->getMethod(),
'uri' => $request->getRequestTarget(),
'body' => (string)$request->getBody(),
]])->getBody();
};
// 返回结果验签,返回可以是4xx,5xx,与远程验签应用约定返回字符串'OK'为验签通过
$remoteVerifier = function (ResponseInterface $response) use ($client, $merchantId): string {
[$nonce] = $response->getHeader('Wechatpay-Nonce');
[$serial] = $response->getHeader('Wechatpay-Serial');
[$signature] = $response->getHeader('Wechatpay-Signature');
[$timestamp] = $response->getHeader('Wechatpay-Timestamp');
return (string)$client->post('/wechatpay-response-merchant-validation', ['json' => [
'mchid' => $merchantId,
'nonce' => $nonce,
'serial' => $serial,
'signature' => $signature,
'timestamp' => $timestamp,
'body' => (string)$response->getBody(),
]])->getBody();
};
$stack = $instance->getDriver()->select()->getConfig('handler');
// 卸载SDK内置签名中间件
$stack->remove('signer');
// 注册内网远程请求签名中间件
$stack->before('prepare_body', Middleware::mapRequest(
static function (RequestInterface $request) use ($remoteSigner): RequestInterface {
return $request->withHeader('Authorization', $remoteSigner($request));
}
), 'signer');
// 卸载SDK内置验签中间件
$stack->remove('verifier');
// 注册内网远程请求验签中间件
$stack->before('http_errors', static function (callable $handler) use ($remoteVerifier): callable {
return static function (RequestInterface $request, array $options = []) use ($remoteVerifier, $handler) {
return $handler($request, $options)->then(
static function(ResponseInterface $response) use ($remoteVerifier, $request): ResponseInterface {
$verified = '';
try {
$verified = $remoteVerifier($response);
} catch (\Throwable $exception) {}
if ($verified === 'OK') { //远程验签约定,返回字符串`OK`作为验签通过
throw new RequestException('签名验签失败', $request, $response, $exception ?? null);
}
return $response;
}
);
};
}, 'verifier');
// 链式/同步/异步请求APIv3即可,例如:
$instance->v3->certificates->getAsync()->then(static function($res) { return $res->getBody(); })->wait();
```
</details>
## 常见问题
### 如何下载平台证书?
使用内置的[微信支付平台证书下载器](bin/README.md)
```bash
composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
微信支付平台证书下载后,下载器会用获得的`平台证书`对返回的消息进行验签。下载器同时开启了 `Guzzle``debug => true` 参数,方便查询请求/响应消息的基础调试信息。
ℹ️ [什么是APIv3密钥?如何设置?](https://kf.qq.com/faq/180830E36vyQ180830AZFZvu.html)
### 证书和回调解密需要的AesGcm解密在哪里?
请参考[AesGcm.php](src/Crypto/AesGcm.php),例如内置的`平台证书`下载工具解密代码如下:
```php
AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data);
```
### 配合swoole使用时,上传文件接口报错
建议升级至swoole 4.6+,swoole在 4.6.0 中增加了native-curl([swoole/swoole-src#3863](https://github.com/swoole/swoole-src/pull/3863))支持,我们测试能正常使用了。
更详细的信息,请参考[#36](https://github.com/wechatpay-apiv3/wechatpay-guzzle-middleware/issues/36)
### 如何加载公/私钥和证书
`v1.2`提供了统一的加载函数 `Rsa::from($thing, $type)`
- `Rsa::from($thing, $type)` 支持从文件/字符串加载公/私钥和证书,使用方法可参考 [RsaTest.php](tests/Crypto/RsaTest.php)
- `Rsa::fromPkcs1`是个语法糖,支持加载 `PKCS#1` 格式的公/私钥,入参是 `base64` 字符串
- `Rsa::fromPkcs8`是个语法糖,支持加载 `PKCS#8` 格式的私钥,入参是 `base64` 字符串
- `Rsa::fromSpki`是个语法糖,支持加载 `SPKI` 格式的公钥,入参是 `base64` 字符串
- `Rsa::pkcs1ToSpki`是个 `RSA公钥` 格式转换函数,入参是 `base64` 字符串
### 如何计算商家券发券 API 的签名
使用 `Hash::sign()`计算 APIv2 的签名,示例请参考 APIv2 文档的 [数据签名](README_APIv2.md#数据签名)
### 为什么 URL 上的变量 OpenID,请求时被替换成小写了?
本 SDK 把 URL 中的大写视为包含连字号的 segment。请求时, `camelCase` 会替换为 `camel-case`。相关 issue 可参考 [#56](https://github.com/wechatpay-apiv3/wechatpay-php/issues/56)[#69](https://github.com/wechatpay-apiv3/wechatpay-php/issues/69)
为了避免大小写错乱,URL 中存在变量时的正确做法是:使用 [链式 URI Template](#%E9%93%BE%E5%BC%8F-uri-template) 的 Path 变量。比如:
- **推荐写法** `->v3->marketing->favor->users->_openid_->coupons->post(['openid' => 'AbcdEF12345'])`
- `->v3->marketing->favor->users->{'{openid}'}->coupons->post(['openid' => 'AbcdEF12345'])`
- `->chain('{+myurl}')->post(['myurl' => 'v3/marketing/favor/users/AbcdEF12345/coupons'])`
- `->{'{+myurl}'}->post(['myurl' => 'v3/marketing/favor/users/AbcdEF12345/coupons'])`
## 联系我们
如果你发现了**BUG**或者有任何疑问、建议,请通过issue进行反馈。
也欢迎访问我们的[开发者社区](https://developers.weixin.qq.com/community/pay)
## 链接
+ [GuzzleHttp官方版本支持](https://docs.guzzlephp.org/en/stable/overview.html#requirements)
+ [PHP官方版本支持](https://www.php.net/supported-versions.php)
+ [变更历史](CHANGELOG.md)
+ [升级指南](UPGRADING.md)
+ <a name="note-rfc3986"></a> [RFC3986](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3)
> section-3.3 `segments`: A path consists of a sequence of path segments separated by a slash ("/") character.
+ <a name="note-rfc6570"><a> [RFC6570](https://www.rfc-editor.org/rfc/rfc6570.html)
+ [PHP密钥/证书参数 相关说明](https://www.php.net/manual/zh/openssl.certparams.php)
## License
[Apache-2.0 License](LICENSE)
# API v2
本类库可单独用于`APIv2`的开发,希望能给商户提供一个过渡,可先平滑迁移至本类库以承接`APIv2`对接,然后再按需替换升级至`APIv3`上。
以下代码以单独使用展开示例,供商户参考。关于链式 `->`,请先阅读 [链式 URI Template](README.md#链式-uri-template)
## V2初始化
```php
use WeChatPay\Builder;
// 商户号,假定为`1000100`
$merchantId = '1000100';
// APIv2密钥(32字节) 假定为`exposed_your_key_here_have_risks`,使用请替换为实际值
$apiv2Key = 'exposed_your_key_here_have_risks';
// 商户私钥,文件路径假定为 `/path/to/merchant/apiclient_key.pem`
$merchantPrivateKeyFilePath = '/path/to/merchant/apiclient_key.pem';
// 商户证书,文件路径假定为 `/path/to/merchant/apiclient_cert.pem`
$merchantCertificateFilePath = '/path/to/merchant/apiclient_cert.pem';
// 工厂方法构造一个实例
$instance = Builder::factory([
'mchid' => $merchantId,
'serial' => 'nop',
'privateKey' => 'any',
'certs' => ['any' => null],
'secret' => $apiv2Key,
'merchant' => [
'cert' => $merchantCertificateFilePath,
'key' => $merchantPrivateKeyFilePath,
],
]);
```
初始化字典说明如下:
- `mchid` 为你的`商户号`,一般是10字节纯数字
- `serial` 为你的`商户证书序列号`,不使用APIv3可填任意值
- `privateKey` 为你的`商户API私钥`,不使用APIv3可填任意值
- `certs[$serial_number => #resource]` 不使用APIv3可填任意值, `$serial_number` 注意不要与商户证书序列号`serial`相同
- `secret` 为APIv2版的`密钥`,商户平台上设置的32字节字符串
- `merchant[cert => $path]` 为你的`商户证书`,一般是文件名为`apiclient_cert.pem`文件路径,接受`[$path, $passphrase]` 格式,其中`$passphrase`为证书密码
- `merchant[key => $path]` 为你的`商户API私钥`,一般是通过官方证书生成工具生成的文件名是`apiclient_key.pem`文件路径,接受`[$path, $passphrase]` 格式,其中`$passphrase`为私钥密码
**注:** `APIv3`, `APIv2` 以及 `GuzzleHttp\Client``$config = []` 初始化参数,均融合在一个型参上。
## 企业付款到零钱
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2)
```php
use WeChatPay\Transformer;
$res = $instance
->v2->mmpaymkttransfers->promotion->transfers
->postAsync([
'xml' => [
'mch_appid' => 'wx8888888888888888',
'mchid' => '1900000109',// 注意这个商户号,key是`mchid`非`mch_id`
'partner_trade_no' => '10000098201411111234567890',
'openid' => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',
'check_name' => 'FORCE_CHECK',
're_user_name' => '王小王',
'amount' => '10099',
'desc' => '理赔',
'spbill_create_ip' => '192.168.0.1',
],
'security' => true, //请求需要双向证书
'debug' => true //开启调试模式
])
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->otherwise(static function($e) {
// 更多`$e`异常类型判断是必须的,这里仅列出一种可能情况,请根据实际对接过程调整并增加
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
return Transformer::toArray((string)$e->getReason()->getBody());
}
return [];
})
->wait();
print_r($res);
```
`APIv2`末尾驱动的 `HTTP METHOD(POST)` 方法入参 `array $options`,可接受类库定义的两个参数,释义如下:
- `$options['nonceless']` - 标量 `scalar` 任意值,语义上即,本次请求不用自动添加`nonce_str`参数,推荐 `boolean(True)`
- `$options['security']` - 布尔量`True`,语义上即,本次请求需要加载ssl证书,对应的是初始化 `array $config['merchant']` 结构体
## 企业付款到银行卡-获取RSA公钥
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=24_7&index=4)
```php
use WeChatPay\Transformer;
$res = $instance
->v2->risk->getpublickey
->postAsync([
'xml' => [
'mch_id' => '1900000109',
'sign_type' => 'MD5',
],
'security' => true, //请求需要双向证书
// 特殊接入点,仅对本次请求有效
'base_uri' => 'https://fraud.mch.weixin.qq.com/',
])
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);
```
## 付款到银行卡
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=24_2)
```php
use WeChatPay\Transformer;
use WeChatPay\Crypto\Rsa;
// 做一个匿名方法,供后续方便使用,$rsaPubKeyString 是`risk/getpublickey` 的返回值'pub_key'字符串
$rsaPublicKeyInstance = Rsa::from($rsaPubKeyString, Rsa::KEY_TYPE_PUBLIC);
$encryptor = static function(string $msg) use ($rsaPublicKeyInstance): string {
return Rsa::encrypt($msg, $rsaPublicKeyInstance);
};
$res = $instance
->v2->mmpaysptrans->pay_bank
->postAsync([
'xml' => [
'mch_id' => '1900000109',
'partner_trade_no' => '1212121221227',
'enc_bank_no' => $encryptor('6225............'),
'enc_true_name' => $encryptor('张三'),
'bank_code' => '1001',
'amount' => '100000',
'desc' => '理财',
],
'security' => true, //请求需要双向证书
])
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);
```
SDK自v1.4.6调整了XML转换函数,支持了[Stringable](https://www.php.net/manual/zh/class.stringable.php) 值的转换,对于 [Scalar](https://www.php.net/manual/zh/function.is-scalar.php) 标量值及实现了 [__toString](https://www.php.net/manual/zh/language.types.string.php#language.types.string.casting) 方法的对象,均支持直接转换,详细可参考 [TransformerTest.php](tests/TransformerTest.php) 的用例用法示例。
## 刷脸支付-人脸识别-获取调用凭证
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/wxfacepay/develop/android/faceuser.html)
```php
use WeChatPay\Formatter;
use WeChatPay\Transformer;
$res = $instance
->v2->face->get_wxpayface_authinfo
->postAsync([
'xml' => [
'store_id' => '1234567',
'store_name' => '云店(广州白云机场店)',
'device_id' => 'abcdef',
'rawdata' => '从客户端`getWxpayfaceRawdata`方法取得的数据',
'appid' => 'wx8888888888888888',
'mch_id' => '1900000109',
'now' => (string)Formatter::timestamp(),
'version' => '1',
'sign_type' => 'HMAC-SHA256',
],
// 特殊接入点,仅对本次请求有效
'base_uri' => 'https://payapp.weixin.qq.com/',
])
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);
```
## v2沙箱环境-获取验签密钥API
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=23_1&index=2)
```php
use WeChatPay\Transformer;
$res = $instance
->v2->xdc->apiv2getsignkey->sign->getsignkey
->postAsync([
'xml' => [
'mch_id' => '1900000109',
],
// 通知SDK不接受沙箱环境重定向,仅对本次请求有效
'allow_redirects' => false,
])
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);
```
## v2通知应答
```php
use WeChatPay\Transformer;
$xml = Transformer::toXml([
'return_code' => 'SUCCESS',
'return_msg' => 'OK',
]);
echo $xml;
```
## 数据签名
### 商家券-小程序发券APIv2密钥签名
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_3_1.shtml)
```php
use WeChatPay\Formatter;
use WeChatPay\Crypto\Hash;
$apiv2Key = 'exposed_your_key_here_have_risks';
$busiFavorFlat = static function (array $params): array {
$result = ['send_coupon_merchant' => $params['send_coupon_merchant']];
foreach ($params['send_coupon_params'] as $index => $item) {
foreach ($item as $key => $value) {
$result["{$key}{$index}"] = $value;
}
}
return $result;
};
// 发券小程序所需数据结构
$busiFavor = [
'send_coupon_params' => [
['out_request_no' => '1234567', 'stock_id' => 'abc123'],
['out_request_no' => '7654321', 'stock_id' => '321cba'],
],
'send_coupon_merchant' => '10016226'
];
$busiFavor += ['sign' => Hash::sign(
Hash::ALGO_HMAC_SHA256,
Formatter::queryStringLike(Formatter::ksort($busiFavorFlat($busiFavor))),
$apiv2Key
)];
echo json_encode($busiFavor);
```
### 商家券-H5发券APIv2密钥签名
[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_4_1.shtml)
```php
use WeChatPay\Formatter;
use WeChatPay\Crypto\Hash;
$apiv2Key = 'exposed_your_key_here_have_risks';
$params = [
'stock_id' => '12111100000001',
'out_request_no' => '20191204550002',
'send_coupon_merchant' => '10016226',
'open_id' => 'oVvBvwEurkeUJpBzX90-6MfCHbec',
'coupon_code' => '75345199',
];
$params += ['sign' => Hash::sign(
Hash::ALGO_HMAC_SHA256,
Formatter::queryStringLike(Formatter::ksort($params)),
$apiv2Key
)];
echo json_encode($params);
```
## v2回调通知
回调通知受限于开发者/商户所使用的`WebServer`有很大差异,这里只给出开发指导步骤,供参考实现。
1. 从请求头`Headers`获取`Request-ID`,商户侧`Web`解决方案可能有差异,请求头的`Request-ID`可能大小写不敏感,请根据自身应用来定;
2. 获取请求`body`体的`XML`纯文本;
3. 调用`SDK`内置方法,根据[签名算法](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3)做本地数据签名计算,然后与通知文本的`sign``Hash::equals`对比验签;
4. 消息体需要解密的,调用`SDK`内置方法解密;
5. 如遇到问题,请拿`Request-ID`点击[这里](https://support.pay.weixin.qq.com/online-service?utm_source=github&utm_medium=wechatpay-php&utm_content=apiv2),联系官方在线技术支持;
样例代码如下:
```php
use WeChatPay\Transformer;
use WeChatPay\Crypto\Hash;
use WeChatPay\Crypto\AesEcb;
use WeChatPay\Formatter;
$inBody = '';// 请根据实际情况获取,例如: file_get_contents('php://input');
$apiv2Key = '';// 在商户平台上设置的APIv2密钥
$inBodyArray = Transformer::toArray($inBody);
// 部分通知体无`sign_type`,部分`sign_type`默认为`MD5`,部分`sign_type`默认为`HMAC-SHA256`
// 部分通知无`sign`字典
// 请根据官方开发文档确定
['sign_type' => $signType, 'sign' => $sign] = $inBodyArray;
$calculated = Hash::sign(
$signType ?? Hash::ALGO_MD5,// 如没获取到`sign_type`,假定默认为`MD5`
Formatter::queryStringLike(Formatter::ksort($inBodyArray)),
$apiv2Key
);
$signatureStatus = Hash::equals($calculated, $sign);
if ($signatureStatus) {
// 如需要解密的
['req_info' => $reqInfo] = $inBodyArray;
$inBodyReqInfoXml = AesEcb::decrypt($reqInfo, Hash::md5($apiv2Key));
$inBodyReqInfoArray = Transformer::toArray($inBodyReqInfoXml);
// print_r($inBodyReqInfoArray);// 打印解密后的结果
}
```
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x | :white_check_mark: |
## Reporting a Vulnerability
Please do not open GitHub issues or pull requests - this makes the problem immediately visible to everyone, including malicious actors.
Security issues in this open source project can be safely reported to [TSRC](https://security.tencent.com).
## 报告漏洞
请不要使用 GitHub issues 或 pull request —— 这会让漏洞立即暴露给所有人,包括恶意人员。
请将本开源项目的安全问题报告给 [腾讯安全应急响应中心](https://security.tencent.com).
---
另外,你可能需要关注影响本SDK运行时行为的主要的PHP扩展缺陷列表:
+ [OpenSSL](https://www.openssl.org/news/vulnerabilities.html)
+ [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/NEWS)
+ [curl](https://curl.se/docs/security.html)
当你准备在报告安全问题时,请先对照如上列表,确认是否存在已知运行时环境安全问题。
当你尝试升级主要扩展至最新版本之后,如若问题依旧存在,请将本开源项目的安全问题报告给 [TSRC腾讯安全应急响应中心](https://security.tencent.com),致谢。
# 升级指南
## 从 1.3 升级至 1.4
v1.4版,对`Guzzle6`提供了**有限**兼容支持,最低可兼容至**v6.5.0**,原因是测试依赖的前向兼容`GuzzleHttp\Handler\MockHandler::reset()`方法,在这个版本上才可用,相关见 [Guzzle#2143](https://github.com/guzzle/guzzle/pull/2143)
`Guzzle6`的PHP版本要求是 **>=5.5**,而本类库前向兼容时,读取RSA证书序列号用到了PHP的[#7151 serialNumberHex support](http://bugs.php.net/71519)功能,顾PHP的最低版本可降级至**7.1.2**这个版本;
**有限**兼容**Guzzle6**,类库放弃使用`Guzzle7`上的`\GuzzleHttp\Utils::jsonEncode``\GuzzleHttp\Utils::jsonDecode`封装方法,取而代之为PHP原生`json_encode`/`json_decode`方法,极端情况下(`meta`数据非法)可能会在`APIv3媒体文件上传`的几个接口上,本该抛送客户端异常而代之为返回服务端异常;这种场景下,会对调试带来部分困难,评估下来可控,遂放弃使用`\GuzzleHttp\Utils`的封装,待`Guzzle6 EOL`时,再择机回滚至使用这两个封装方法。
**警告**:PHP7.1已于*1 Dec 2019*完成其**PHP官方支持**生命周期,本类库在PHP7.1环境上也仅有限支持可用,请**商户/开发者**自行评估继续使用PHP7.1的风险。
同时,测试用例依赖的`PHPUnit8`调整最低版本至**v8.5.16**,原因是本类库的前向用例覆盖用到了`TestCase::expectError`方法,其在PHP7.4/8.0上有[bug #4663](https://github.com/sebastianbergmann/phpunit/issues/4663),顾调整至这个版本。
Guzzle7+PHP7.2/7.3/7.4/8.0环境下,本次版本升级不受影响。
## 从 1.2 升级到 1.3
v1.3主要更新内容是为IDE增加`接口``参数`描述提示,以单独的安装包发行,建议仅在`composer --dev`即(`Add requirement to require-dev.`),生产运行时环境完全无需。
## 从 1.1 升级至 1.2
v1.2 对 `RSA公/私钥`加载做了加强,释放出 `Rsa::from` 统一加载函数,以接替`PemUtil::loadPrivateKey`,同时释放出`Rsa::fromPkcs1`, `Rsa::fromPkcs8`, `Rsa::fromSpki``Rsa::pkcs1ToSpki`方法,在不丢失精度的前提下,支持`不落盘`从云端(如`公/私钥`存储在数据库/NoSQL等媒介中)加载。
- `Rsa::from` 支持从文件/字符串/完整RSA公私钥字符串/X509证书加载,对应的测试用例覆盖见[这里](tests/Crypto/RsaTest.php);
- `Rsa::fromPkcs1`是个语法糖,支持加载`PKCS#1`格式的公/私钥,入参是`base64`字符串;
- `Rsa::fromPkcs8`是个语法糖,支持加载`PKCS#8`格式的私钥,入参是`base64`字符串;
- `Rsa::fromSpki`是个语法糖,支持加载`SPKI`格式的公钥,入参是`base64`字符串;
- `Rsa::pkcs1ToSpki`是个`RSA公钥`格式转换函数,入参是`base64`字符串;
特别地,对于`APIv2` 付款到银行卡功能,现在可直接支持`加密敏感信息`了,即从[获取RSA加密公钥](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=24_7&index=4)接口获取的`pub_key`字符串,经`Rsa::from($pub_key, Rsa::KEY_TYPE_PUBLIC)`加载,用于`Rsa::encrypt`加密,详细用法见README示例;
标记 `PemUtil::loadPrivateKey``PemUtil::loadPrivateKeyFromString``不推荐用法`,当前向下兼容v1.1及v1.0版本用法,预期在v2.0大版本上会移除这两个方法;
推荐升级加载`RSA公/私钥`为以下形式:
从文件加载「商户RSA私钥」,变化如下:
```diff
+use WeChatPay\Crypto\Rsa;
-$merchantPrivateKeyFilePath = '/path/to/merchant/apiclient_key.pem';
-$merchantPrivateKeyInstance = PemUtil::loadPrivateKey($merchantPrivateKeyFilePath);
+$merchantPrivateKeyFilePath = 'file:///path/to/merchant/apiclient_key.pem';// 注意 `file://` 开头协议不能少
+$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
```
从文件加载「平台证书」,变化如下:
```diff
-$platformCertificateFilePath = '/path/to/wechatpay/cert.pem';
-$platformCertificateInstance = PemUtil::loadCertificate($platformCertificateFilePath);
-// 解析平台证书序列号
-$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateInstance);
+$platformCertificateFilePath = 'file:///path/to/wechatpay/cert.pem';// 注意 `file://` 开头协议不能少
+$platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
+// 解析「平台证书」序列号,「平台证书」当前五年一换,缓存后就是个常量
+$platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
```
相对应地初始化工厂方法,平台证书相关入参初始化变化如下:
```diff
'certs' => [
- $platformCertificateSerial => $platformCertificateInstance,
+ $platformCertificateSerial => $platformPublicKeyInstance,
],
```
APIv3相关「RSA数据签名」,变化如下:
```diff
-use WeChatPay\Util\PemUtil;
-$merchantPrivateKeyFilePath = '/path/to/merchant/apiclient_key.pem';
-$merchantPrivateKeyInstance = PemUtil::loadPrivateKey($merchantPrivateKeyFilePath);
+$merchantPrivateKeyFilePath = 'file:///path/to/merchant/apiclient_key.pem';
+$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);
```
APIv3回调通知「验签」,变化如下:
```diff
-use WeChatPay\Util\PemUtil;
// 根据通知的平台证书序列号,查询本地平台证书文件,
// 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
-$certInstance = PemUtil::loadCertificate('/path/to/wechatpay/inWechatpaySerial.pem');
+$platformPublicKeyInstance = Rsa::from('file:///path/to/wechatpay/inWechatpaySerial.pem', Rsa::KEY_TYPE_PUBLIC);
// 检查通知时间偏移量,允许5分钟之内的偏移
$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
$verifiedStatus = Rsa::verify(
// 构造验签名串
Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
$inWechatpaySignature,
- $certInstance
+ $platformPublicKeyInstance
);
```
更高级的加载`RSA公/私钥`方式,如从`Rsa::fromPkcs1``Rsa::fromPkcs8`, `Rsa::fromSpki`等语法糖加载,可查询参考测试用例[RsaTest.php](tests/Crypto/RsaTest.php)做法,请按需自行拓展使用。
## 从 1.0 升级至 1.1
v1.1 版本对内部中间件实现做了微调,对`APIv3的异常`做了部分调整,调整内容如下:
1. 对中间件栈顺序,做了微调,从原先的栈顶调整至必要位置,即:
1. 请求签名中间件 `signer` 从栈顶调整至 `prepare_body` 之前,`请求签名`仅须发生在请求发送体准备阶段之前,这个顺序调整对应用端无感知;
2. 返回验签中间件 `verifier` 从栈顶调整至 `http_errors` 之前(默认实际仍旧在栈顶),对异常(HTTP 4XX, 5XX)返回交由`Guzzle`内置的`\GuzzleHttp\Middleware::httpErrors`进行处理,`返回验签`仅对正常(HTTP 20X)结果验签;
2. 重构了 `verifier` 实现,调整内容如下:
1. 异常类型从 `\UnexpectedValueException` 调整成 `\GuzzleHttp\Exception\RequestException`;因由是,请求/响应已经完成,响应内容有(HTTP 20X)结果,调整后,SDK客户端异常时,可以从`RequestException::getResponse()`获取到这个响应对象,进而可甄别出`返回体`具体内容;
2. 正常响应结果在验签时,有可能从 `\WeChatPay\Crypto\Rsa::verify` 内部抛出`UnexpectedValueException`异常,调整后,一并把这个异常交由`RequestException`抛出,应用侧可以从`RequestException::getPrevious()`获取到这个异常实例;
以上调整,对于正常业务逻辑(HTTP 20X)无影响,对于应用侧异常捕获,需要做如下适配调整:
同步模型,建议从捕获`UnexpectedValueException`调整为`\GuzzleHttp\Exception\RequestException`,如下:
```diff
try {
$instance
->v3->pay->transactions->native
->post(['json' => []]);
- } catch (\UnexpectedValueException $e) {
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
// do something
}
```
异步模型,建议始终判断当前异常是否实例于`\GuzzleHttp\Exception\RequestException`,判断方法见[README](README.md)示例代码。
## 从 wechatpay-guzzle-middleware 0.2 迁移至 1.0
[变更历史](CHANGELOG.md) 所述,本类库自1.0不兼容`wechatpay/wechatpay-guzzle-middleware:~0.2`,原因如下:
1. 升级`Guzzle`大版本至`7`, `Guzzle7`做了许多不兼容更新,相关讨论可见[Laravel8依赖变更](https://github.com/wechatpay-apiv3/wechatpay-guzzle-middleware/issues/54)`Guzzle7`要求PHP最低版本为`7.2.5`,重要特性是加入了`函数参数类型签名`以及`函数返回值类型签名`功能,从开发语言层面,使类库健壮性有了显著提升;
2. 重构并修正了原[敏感信息加解密](https://github.com/wechatpay-apiv3/wechatpay-guzzle-middleware/issues/25)过度设计问题;
3. 重新设计了类库函数及方案,以提供[回调通知签名](https://github.com/wechatpay-apiv3/wechatpay-guzzle-middleware/issues/42)所需方法;
4. 调整`composer.json`移动`guzzlehttp/guzzle``require-dev`弱依赖至`require`强依赖,开发者无须再手动添加;
5. 缩减初始化手动拼接客户端参数至`Builder::factory`,统一由SDK来构建客户端;
6. 新增链式调用封装器,原生提供对`APIv3`的链式调用;
7. 新增`APIv2`支持,推荐商户可以先升级至本类库支持的`APIv2`能力,然后再按需升级至相对应的`APIv3`能力;
8. 增加类库单元测试覆盖`Linux`,`macOS``Windows`运行时;
9. 调整命名空间`namespace``WeChatPay`;
### 迁移指南
PHP版本最低要求为`7.2.5`,请商户的技术开发人员**先评估**运行时环境是否支持**再决定**按如下步骤迁移。
### composer.json 调整
依赖调整
```diff
"require": {
- "guzzlehttp/guzzle": "^6.3",
- "wechatpay/wechatpay-guzzle-middleware": "^0.2.0"
+ "wechatpay/wechatpay": "^1.0"
}
```
### 初始化方法调整
```diff
use GuzzleHttp\Exception\RequestException;
- use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
+ use WeChatPay\Builder;
- use WechatPay\GuzzleMiddleware\Util\PemUtil;
+ use WeChatPay\Util\PemUtil;
$merchantId = '1000100';
$merchantSerialNumber = 'XXXXXXXXXX';
$merchantPrivateKey = PemUtil::loadPrivateKey('/path/to/mch/private/key.pem');
$wechatpayCertificate = PemUtil::loadCertificate('/path/to/wechatpay/cert.pem');
+$wechatpayCertificateSerialNumber = PemUtil::parseCertificateSerialNo($wechatpayCertificate);
- $wechatpayMiddleware = WechatPayMiddleware::builder()
- ->withMerchant($merchantId, $merchantSerialNumber, $merchantPrivateKey)
- ->withWechatPay([ $wechatpayCertificate ])
- ->build();
- $stack = GuzzleHttp\HandlerStack::create();
- $stack->push($wechatpayMiddleware, 'wechatpay');
- $client = new GuzzleHttp\Client(['handler' => $stack]);
+ $instance = Builder::factory([
+ 'mchid' => $merchantId,
+ 'serial' => $merchantSerialNumber,
+ 'privateKey' => $merchantPrivateKey,
+ 'certs' => [$wechatpayCertificateSerialNumber => $wechatpayCertificate],
+ ]);
```
### 调用方法调整
#### **GET**请求
可以使用本SDK提供的语法糖,缩减请求代码结构如下:
```diff
try {
- $resp = $client->request('GET', 'https://api.mch.weixin.qq.com/v3/...', [
+ $resp = $instance->chain('v3/...')->get([
- 'headers' => [ 'Accept' => 'application/json' ]
]);
} catch (RequestException $e) {
//do something
}
```
#### **POST**请求
缩减请求代码如下:
```diff
try {
- $resp = $client->request('POST', 'https://api.mch.weixin.qq.com/v3/...', [
+ $resp = $instance->chain('v3/...')->post([
'json' => [ // JSON请求体
'field1' => 'value1',
'field2' => 'value2'
],
- 'headers' => [ 'Accept' => 'application/json' ]
]);
} catch (RequestException $e) {
//do something
}
```
#### 上传媒体文件
```diff
- use WechatPay\GuzzleMiddleware\Util\MediaUtil;
+ use WeChatPay\Util\MediaUtil;
$media = new MediaUtil('/your/file/path/with.extension');
try {
- $resp = $client->request('POST', 'https://api.mch.weixin.qq.com/v3/[merchant/media/video_upload|marketing/favor/media/image-upload]', [
+ $resp = $instance->chain('v3/marketing/favor/media/image-upload')->post([
'body' => $media->getStream(),
'headers' => [
- 'Accept' => 'application/json',
'content-type' => $media->getContentType(),
]
]);
} catch (Exception $e) {
// do something
}
```
```diff
try {
- $resp = $client->post('merchant/media/upload', [
+ $resp = $instance->chain('v3/merchant/media/upload')->post([
'body' => $media->getStream(),
'headers' => [
- 'Accept' => 'application/json',
'content-type' => $media->getContentType(),
]
]);
} catch (Exception $e) {
// do something
}
```
#### 敏感信息加/解密
```diff
- use WechatPay\GuzzleMiddleware\Util\SensitiveInfoCrypto;
+ use WeChatPay\Crypto\Rsa;
- $encryptor = new SensitiveInfoCrypto(PemUtil::loadCertificate('/path/to/wechatpay/cert.pem'));
+ $encryptor = function($msg) use ($wechatpayCertificate) { return Rsa::encrypt($msg, $wechatpayCertificate); };
try {
- $resp = $client->post('/v3/applyment4sub/applyment/', [
+ $resp = $instance->chain('v3/applyment4sub/applyment/')->post([
'json' => [
'business_code' => 'APL_98761234',
'contact_info' => [
'contact_name' => $encryptor('value of `contact_name`'),
'contact_id_number' => $encryptor('value of `contact_id_number'),
'mobile_phone' => $encryptor('value of `mobile_phone`'),
'contact_email' => $encryptor('value of `contact_email`'),
],
//...
],
'headers' => [
- 'Wechatpay-Serial' => 'must be the serial number via the downloaded pem file of `/v3/certificates`',
+ 'Wechatpay-Serial' => $wechatpayCertificateSerialNumber,
- 'Accept' => 'application/json',
],
]);
} catch (Exception $e) {
// do something
}
```
#### 平台证书下载工具
在第一次下载平台证书时,本类库充分利用了`\GuzzleHttp\HandlerStack`中间件管理器能力,按照栈执行顺序,在返回结果验签中间件`verifier`之前注册`certsInjector`,之后注册`certsRecorder`**"解开"** "死循环"问题。
本类库提供的下载工具**未改变** `返回结果验签` 逻辑,完整实现可参考[bin/CertificateDownloader.php](bin/CertificateDownloader.php)
#### AesGcm平台证书解密
```diff
- use WechatPay\GuzzleMiddleware\Util\AesUtil;
+ use WeChatPay\Crypto\AesGcm;
- $decrypter = new AesUtil($opts['key']);
- $plain = $decrypter->decryptToString($encCert['associated_data'], $encCert['nonce'], $encCert['ciphertext']);
+ $plain = AesGcm::decrypt($encCert['ciphertext'], $opts['key'], $encCert['nonce'], $encCert['associated_data']);
```
## 从 php_sdk_v3.0.10 迁移至 1.0
这个`php_sdk_v3.0.10`版的SDK,是在`APIv2`版的文档上有下载,这里提供一份迁移指南,抛砖引玉如何迁移。
### 初始化
从手动文件模式调整参数,变更为实例初始化方式:
```diff
- // ③、修改lib/WxPay.Config.php为自己申请的商户号的信息(配置详见说明)
+ use WeChatPay/Builder;
+ $instance = new Builder([
+ 'mchid' => $mchid,
+ 'serial' => 'nop',
+ 'privateKey' => 'any',
+ 'secret' => $apiv2Key,
+ 'certs' => ['any' => null],
+ 'merchant' => ['key' => '/path/to/cert/apiclient_key.pem', 'cert' => '/path/to/cert/apiclient_cert.pem'],
+ ]);
```
### 统一下单-JSAPI下单及数据二次签名
```diff
- require_once "../lib/WxPay.Api.php";
- require_once "WxPay.JsApiPay.php";
- require_once "WxPay.Config.php";
- $tools = new JsApiPay();
- $openId = $tools->GetOpenid();
- $input = new WxPayUnifiedOrder();
- $input->SetBody("test");
- $input->SetAttach("test");
- $input->SetOut_trade_no("sdkphp".date("YmdHis"));
- $input->SetTotal_fee("1");
- $input->SetTime_start(date("YmdHis"));
- $input->SetTime_expire(date("YmdHis", time() + 600));
- $input->SetGoods_tag("test");
- $input->SetNotify_url("http://paysdk.weixin.qq.com/notify.php");
- $input->SetTrade_type("JSAPI");
- $input->SetOpenid($openId);
- $config = new WxPayConfig();
- $order = WxPayApi::unifiedOrder($config, $input);
- printf_info($order);
- // 数据签名
- $jsapi = new WxPayJsApiPay();
- $jsapi->SetAppid($order["appid"]);
- $timeStamp = time();
- $jsapi->SetTimeStamp("$timeStamp");
- $jsapi->SetNonceStr(WxPayApi::getNonceStr());
- $jsapi->SetPackage("prepay_id=" . $order['prepay_id']);
- $config = new WxPayConfig();
- $jsapi->SetPaySign($jsapi->MakeSign($config));
- $parameters = json_encode($jsapi->GetValues());
+ use WeChatPay\Formatter;
+ use WeChatPay\Transformer;
+ use WeChatPay\Crypto\Hash;
+ // 直接构造请求数组参数
+ $input = [
+ 'appid' => $appid, // 从config拿到当前请求参数上
+ 'mch_id' => $mchid, // 从config拿到当前请求参数上
+ 'body' => 'test',
+ 'attach' => 'test',
+ 'out_trade_no' => 'sdkphp' . date('YmdHis'),
+ 'total_fee' => '1',
+ 'time_start' => date('YmdHis'),
+ 'time_expire' => date('YmdHis, time() + 600),
+ 'goods_tag' => 'test',
+ 'notify_url' => 'http://paysdk.weixin.qq.com/notify.php',
+ 'trade_type' => 'JSAPI',
+ 'openid' => $openId, // 有太多优秀解决方案能够获取到这个值,这里假定已经有了
+ 'sign_type' => Hash::ALGO_HMAC_SHA256, // 以下二次数据签名「签名类型」需与预下单数据「签名类型」一致
+ ];
+ // 发起请求并取得结果,抑制`E_USER_DEPRECATED`提示
+ $resp = @$instance->chain('v2/pay/unifiedorder')->post(['xml' => $input]);
+ $order = Transformer::toArray((string)$resp->getBody());
+ // print_r($order);
+ // 数据签名
+ $params = [
+ 'appId' => $appid,
+ 'timeStamp' => (string)Formatter::timestamp(),
+ 'nonceStr' => Formatter::nonce(),
+ 'package' => 'prepay_id=' . $order['prepay_id'],
+ 'signType' => Hash::ALGO_HMAC_SHA256,
+ ];
+ // 二次数据签名「签名类型」需与预下单数据「签名类型」一致
+ $params['paySign'] = Hash::sign(Hash::ALGO_HMAC_SHA256, Formatter::queryStringLike(Formatter::ksort($parameters)), $apiv2Key);
+ $parameters = json_encode($params);
```
### 付款码支付
```diff
- require_once "../lib/WxPay.Api.php";
- require_once "WxPay.MicroPay.php";
-
- $auth_code = $_REQUEST["auth_code"];
- $input = new WxPayMicroPay();
- $input->SetAuth_code($auth_code);
- $input->SetBody("刷卡测试样例-支付");
- $input->SetTotal_fee("1");
- $input->SetOut_trade_no("sdkphp".date("YmdHis"));
-
- $microPay = new MicroPay();
- printf_info($microPay->pay($input));
+ use WeChatPay\Formatter;
+ use WeChatPay\Transformer;
+ // 直接构造请求数组参数
+ $input = [
+ 'appid' => $appid, // 从config拿到当前请求参数上
+ 'mch_id' => $mchid, // 从config拿到当前请求参数上
+ 'auth_code' => $auth_code,
+ 'body' => '刷卡测试样例-支付',
+ 'total_fee' => '1',
+ 'out_trade_no' => 'sdkphp' . date('YmdHis'),
+ 'spbill_create_ip' => $mechineIp,
+ ];
+ // 发起请求并取得结果,抑制`E_USER_DEPRECATED`提示
+ $resp = @$instance->chain('v2/pay/micropay')->post(['xml' => $input]);
+ $order = Transformer::toArray((string)$resp->getBody());
+ // print_r($order);
```
### 撤销订单
```diff
+ $input = [
+ 'appid' => $appid, // 从config拿到当前请求参数上
+ 'mch_id' => $mchid, // 从config拿到当前请求参数上
+ 'out_trade_no' => $outTradeNo,
+ ];
+ // 发起请求并取得结果,抑制`E_USER_DEPRECATED`提示
+ $resp = @$instance->chain('v2/secapi/pay/reverse')->postAsync(['xml' => $input, 'security' => true])->wait();
+ $result = Transformer::toArray((string)$resp->getBody());
+ // print_r($result);
```
其他`APIv2`迁移及接口请求类似如上,示例仅做了正常返回样例,**程序缜密性,需要加入`try catch`/`otherwise`结构捕获异常情况**
至此,迁移后,`Chainable``PromiseA+`以及强劲的`PHP8`运行时,均可愉快地调用微信支付官方接口了。
#!/usr/bin/env php
<?php declare(strict_types=1);
// load autoload.php
$possibleFiles = [__DIR__.'/../vendor/autoload.php', __DIR__.'/../../../autoload.php', __DIR__.'/../../autoload.php'];
$file = null;
foreach ($possibleFiles as $possibleFile) {
if (\file_exists($possibleFile)) {
$file = $possibleFile;
break;
}
}
if (null === $file) {
throw new \RuntimeException('Unable to locate autoload.php file.');
}
require_once $file;
unset($possibleFiles, $possibleFile, $file);
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use WeChatPay\Builder;
use WeChatPay\ClientDecoratorInterface;
use WeChatPay\Crypto\AesGcm;
/**
* CertificateDownloader class
*/
class CertificateDownloader
{
private const DEFAULT_BASE_URI = 'https://api.mch.weixin.qq.com/';
public function run(): void
{
$opts = $this->parseOpts();
if (!$opts || isset($opts['help'])) {
$this->printHelp();
return;
}
if (isset($opts['version'])) {
self::prompt(ClientDecoratorInterface::VERSION);
return;
}
$this->job($opts);
}
/**
* Before `verifier` executing, decrypt and put the platform certificate(s) into the `$certs` reference.
*
* @param string $apiv3Key
* @param array<string,?string> $certs
*
* @return callable(ResponseInterface)
*/
private static function certsInjector(string $apiv3Key, array &$certs): callable {
return static function(ResponseInterface $response) use ($apiv3Key, &$certs): ResponseInterface {
$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;
};
}
/**
* @param array<string,string|true> $opts
*
* @return void
*/
private function job(array $opts): void
{
static $certs = ['any' => null];
$outputDir = $opts['output'] ?? \sys_get_temp_dir();
$apiv3Key = (string) $opts['key'];
$instance = Builder::factory([
'mchid' => $opts['mchid'],
'serial' => $opts['serialno'],
'privateKey' => \file_get_contents((string)$opts['privatekey']),
'certs' => &$certs,
'base_uri' => (string)($opts['baseuri'] ?? self::DEFAULT_BASE_URI),
]);
/** @var \GuzzleHttp\HandlerStack $stack */
$stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler');
// The response middle stacks were executed one by one on `FILO` order.
$stack->after('verifier', Middleware::mapResponse(self::certsInjector($apiv3Key, $certs)), 'injector');
$stack->before('verifier', Middleware::mapResponse(self::certsRecorder((string) $outputDir, $certs)), 'recorder');
$instance->chain('v3/certificates')->getAsync(
['debug' => true]
)->otherwise(static function($exception) {
self::prompt($exception->getMessage());
if ($exception instanceof RequestException && $exception->hasResponse()) {
/** @var ResponseInterface $response */
$response = $exception->getResponse();
self::prompt((string) $response->getBody(), '', '');
}
self::prompt($exception->getTraceAsString());
})->wait();
}
/**
* After `verifier` executed, wrote the platform certificate(s) onto disk.
*
* @param string $outputDir
* @param array<string,?string> $certs
*
* @return callable(ResponseInterface)
*/
private static function certsRecorder(string $outputDir, array &$certs): callable {
return static function(ResponseInterface $response) use ($outputDir, &$certs): ResponseInterface {
$body = (string) $response->getBody();
$json = \json_decode($body);
$data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
\array_walk($data, static function($row, $index, $certs) use ($outputDir) {
$serialNo = $row->serial_no;
$outpath = $outputDir . \DIRECTORY_SEPARATOR . 'wechatpay_' . $serialNo . '.pem';
self::prompt(
'Certificate #' . $index . ' {',
' Serial Number: ' . self::highlight($serialNo),
' Not Before: ' . (new \DateTime($row->effective_time))->format(\DateTime::W3C),
' Not After: ' . (new \DateTime($row->expire_time))->format(\DateTime::W3C),
' Saved to: ' . self::highlight($outpath),
' You may confirm the above infos again even if this library already did(by Crypto\Rsa::verify):',
' ' . self::highlight(\sprintf('openssl x509 -in %s -noout -serial -dates', $outpath)),
' Content: ', '', $certs[$serialNo] ?? '', '',
'}'
);
\file_put_contents($outpath, $certs[$serialNo]);
}, $certs);
return $response;
};
}
/**
* @param string $thing
*/
private static function highlight(string $thing): string
{
return \sprintf("\x1B[1;32m%s\x1B[0m", $thing);
}
/**
* @param string $messages
*/
private static function prompt(...$messages): void
{
\array_walk($messages, static function (string $message): void { \printf('%s%s', $message, \PHP_EOL); });
}
/**
* @return ?array<string,string|true>
*/
private function parseOpts(): ?array
{
$opts = [
[ 'key', 'k', true ],
[ 'mchid', 'm', true ],
[ 'privatekey', 'f', true ],
[ 'serialno', 's', true ],
[ 'output', 'o', false ],
// baseuri can be one of 'https://api2.mch.weixin.qq.com/', 'https://apihk.mch.weixin.qq.com/'
[ 'baseuri', 'u', false ],
];
$shortopts = 'hV';
$longopts = [ 'help', 'version' ];
foreach ($opts as $opt) {
[$key, $alias] = $opt;
$shortopts .= $alias . ':';
$longopts[] = $key . ':';
}
$parsed = \getopt($shortopts, $longopts);
if (!$parsed) {
return null;
}
$args = [];
foreach ($opts as $opt) {
[$key, $alias, $mandatory] = $opt;
if (isset($parsed[$key]) || isset($parsed[$alias])) {
/** @var string|string[] $possible */
$possible = $parsed[$key] ?? $parsed[$alias] ?? '';
$args[$key] = \is_array($possible) ? $possible[0] : $possible;
} elseif ($mandatory) {
return null;
}
}
if (isset($parsed['h']) || isset($parsed['help'])) {
$args['help'] = true;
}
if (isset($parsed['V']) || isset($parsed['version'])) {
$args['version'] = true;
}
return $args;
}
private function printHelp(): void
{
self::prompt(
'Usage: 微信支付平台证书下载工具 [-hV]',
' -f=<privateKeyFilePath> -k=<apiv3Key> -m=<merchantId>',
' -s=<serialNo> -o=[outputFilePath] -u=[baseUri]',
'Options:',
' -m, --mchid=<merchantId> 商户号',
' -s, --serialno=<serialNo> 商户证书的序列号',
' -f, --privatekey=<privateKeyFilePath>',
' 商户的私钥文件',
' -k, --key=<apiv3Key> APIv3密钥',
' -o, --output=[outputFilePath]',
' 下载成功后保存证书的路径,可选,默认为临时文件目录夹',
' -u, --baseuri=[baseUri] 接入点,可选,默认为 ' . self::DEFAULT_BASE_URI,
' -V, --version Print version information and exit.',
' -h, --help Show this help message and exit.', ''
);
}
}
// main
(new CertificateDownloader())->run();
# Certificate Downloader
Certificate Downloader 是 PHP版 微信支付 APIv3 平台证书的命令行下载工具。该工具可从 `https://api.mch.weixin.qq.com/v3/certificates` 接口获取商户可用证书,并使用 [APIv3 密钥](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/api-v3-mi-yao) 和 AES_256_GCM 算法进行解密,并把解密后证书下载到指定位置。
## 使用
使用方法与 [Java版Certificate Downloader](https://github.com/wechatpay-apiv3/CertificateDownloader) 一致,参数与常见问题请参考[其文档](https://github.com/wechatpay-apiv3/CertificateDownloader/blob/master/README.md)
```shell
> bin/CertificateDownloader.php
Usage: 微信支付平台证书下载工具 [-hV]
-f=<privateKeyFilePath> -k=<apiV3key> -m=<merchantId>
-s=<serialNo> -o=[outputFilePath] -u=[baseUri]
Options:
-m, --mchid=<merchantId> 商户号
-s, --serialno=<serialNo> 商户证书的序列号
-f, --privatekey=<privateKeyFilePath>
商户的私钥文件
-k, --key=<apiV3key> ApiV3Key
-o, --output=[outputFilePath]
下载成功后保存证书的路径,可选参数,默认为临时文件目录夹
-u, --baseuri=[baseUri] 接入点,默认为 https://api.mch.weixin.qq.com/
-V, --version Print version information and exit.
-h, --help Show this help message and exit.
```
完整命令示例:
```shell
./bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
```shell
php -f ./bin/CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
```shell
php ./bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
使用`composer`安装的软件包,可以通过如下命令下载:
```shell
vendor/bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
```shell
composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
使用源码克隆版本,也可以使用`composer`通过以下命令下载:
```shell
composer v3-certificates -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
支持从海外接入点下载,命令如下:
```shell
composer v3-certificates -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath} -u https://apihk.mch.weixin.qq.com/
```
**注:** 示例命令行上的`${}`是变量表达方法,运行时请替换(包括`${}`)为对应的实际值。
## 常见问题
### 如何保证证书正确
请参见CertificateDownloader文档中[关于如何保证证书正确的说明](https://github.com/wechatpay-apiv3/CertificateDownloader#%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E8%AF%81%E4%B9%A6%E6%AD%A3%E7%A1%AE)
### 如何使用信任链验证平台证书
请参见CertificateDownloader文档中[关于如何使用信任链验证平台证书的说明](https://github.com/wechatpay-apiv3/CertificateDownloader#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E4%BF%A1%E4%BB%BB%E9%93%BE%E9%AA%8C%E8%AF%81%E5%B9%B3%E5%8F%B0%E8%AF%81%E4%B9%A6)
### 第一次下载证书
请参见CertificateDownloader文档中[相关说明](https://github.com/wechatpay-apiv3/CertificateDownloader#%E7%AC%AC%E4%B8%80%E6%AC%A1%E4%B8%8B%E8%BD%BD%E8%AF%81%E4%B9%A6)
{
"name": "wechatpay/wechatpay",
"version": "1.4.12",
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"type": "library",
"keywords": [
"wechatpay",
"openapi-chainable",
"xml-parser",
"xml-builder",
"aes-ecb",
"aes-gcm",
"rsa-oaep"
],
"authors": [
{
"name": "James ZHANG",
"homepage": "https://github.com/TheNorthMemory"
},
{
"name": "WeChatPay Community",
"homepage": "https://developers.weixin.qq.com/community/pay"
}
],
"homepage": "https://pay.weixin.qq.com/",
"license": "Apache-2.0",
"require": {
"php": ">=7.1.2",
"ext-curl": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-openssl": "*",
"guzzlehttp/uri-template": "^0.2 || ^1.0",
"guzzlehttp/guzzle": "^6.5 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5",
"phpstan/phpstan": "^0.12.89 || ^1.0"
},
"autoload": {
"psr-4": { "WeChatPay\\" : "src/" }
},
"autoload-dev": {
"psr-4": { "WeChatPay\\Tests\\" : "tests/" }
},
"bin": [
"bin/CertificateDownloader.php"
],
"scripts": {
"v3-certificates": "bin/CertificateDownloader.php"
}
}
includes:
- phpstan.v8.2.neon
parameters:
ignoreErrors:
-
message: "#^(?:Left|Right) side of && is always true#"
path: src/Crypto/Hash.php
<?php declare(strict_types=1);
namespace WeChatPay;
use function preg_replace_callback_array;
use function strtolower;
use function implode;
use function array_filter;
use ArrayIterator;
/**
* Chainable the client for sending HTTP requests.
*/
final class Builder
{
/**
* Building & decorate the chainable `GuzzleHttp\Client`
*
* Minimum mandatory \$config parameters structure
* - mchid: string - The merchant ID
* - serial: string - The serial number of the merchant certificate
* - privateKey: \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string - The merchant private key.
* - certs: array<string, \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string> - The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
* - secret?: string - The secret key string (optional)
* - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
* - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
* - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
*
* ```php
* // usage samples
* $instance = Builder::factory([]);
* $res = $instance->chain('v3/merchantService/complaintsV2')->get(['debug' => true]);
* $res = $instance->chain('v3/merchant-service/complaint-notifications')->get(['debug' => true]);
* $instance->v3->merchantService->ComplaintNotifications->postAsync([])->wait();
* $instance->v3->certificates->getAsync()->then(function() {})->otherwise(function() {})->wait();
* ```
*
* @param array<string,string|int|bool|array|mixed> $config - `\GuzzleHttp\Client`, `APIv3` and `APIv2` configuration settings.
*/
public static function factory(array $config = []): BuilderChainable
{
return new class([], new ClientDecorator($config)) extends ArrayIterator implements BuilderChainable
{
use BuilderTrait;
/**
* Compose the chainable `ClientDecorator` instance, most starter with the tree root point
* @param string[] $input
* @param ?ClientDecoratorInterface $instance
*/
public function __construct(array $input = [], ?ClientDecoratorInterface $instance = null) {
parent::__construct($input, self::STD_PROP_LIST | self::ARRAY_AS_PROPS);
$this->setDriver($instance);
}
/**
* @var ClientDecoratorInterface $driver - The `ClientDecorator` instance
*/
protected $driver;
/**
* `$driver` setter
* @param ClientDecoratorInterface $instance - The `ClientDecorator` instance
*/
public function setDriver(ClientDecoratorInterface &$instance): BuilderChainable
{
$this->driver = $instance;
return $this;
}
/**
* @inheritDoc
*/
public function getDriver(): ClientDecoratorInterface
{
return $this->driver;
}
/**
* Normalize the `$thing` by the rules: `PascalCase` -> `camelCase`
* & `camelCase` -> `kebab-case`
* & `_placeholder_` -> `{placeholder}`
*
* @param string $thing - The string waiting for normalization
*
* @return string
*/
protected function normalize(string $thing = ''): string
{
return preg_replace_callback_array([
'#^[A-Z]#' => static function(array $piece): string { return strtolower($piece[0]); },
'#[A-Z]#' => static function(array $piece): string { return '-' . strtolower($piece[0]); },
'#^_(.*)_$#' => static function(array $piece): string { return '{' . $piece[1] . '}'; },
], $thing) ?? $thing;
}
/**
* URI pathname
*
* @param string $seperator - The URI seperator, default is slash(`/`) character
*
* @return string - The URI string
*/
protected function pathname(string $seperator = '/'): string
{
return implode($seperator, $this->simplized());
}
/**
* Only retrieve a copy array of the URI segments
*
* @return string[] - The URI segments array
*/
protected function simplized(): array
{
return array_filter($this->getArrayCopy(), static function($v) { return !($v instanceof BuilderChainable); });
}
/**
* @inheritDoc
*/
public function offsetGet($key): BuilderChainable
{
if (!$this->offsetExists($key)) {
$indices = $this->simplized();
$indices[] = $this->normalize($key);
$this->offsetSet($key, new self($indices, $this->getDriver()));
}
return parent::offsetGet($key);
}
/**
* @inheritDoc
*/
public function chain(string $segment): BuilderChainable
{
return $this->offsetGet($segment);
}
};
}
private function __construct()
{
// cannot be instantiated
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Signature of the Chainable `GuzzleHttp\Client` interface
* @property-read OpenAPI\V2 $v2 - The entrance of the APIv2 endpoint
* @property-read OpenAPI\V3 $v3 - The entrance of the APIv3 endpoint
*/
interface BuilderChainable
{
/**
* `$driver` getter
*/
public function getDriver(): ClientDecoratorInterface;
/**
* Chainable the given `$segment` with the `ClientDecoratorInterface` instance
*
* @param string $segment - The sgement or `URI`
*/
public function chain(string $segment): BuilderChainable;
/**
* Create and send an HTTP GET request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function get(array $options = []): ResponseInterface;
/**
* Create and send an HTTP PUT request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function put(array $options = []): ResponseInterface;
/**
* Create and send an HTTP POST request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function post(array $options = []): ResponseInterface;
/**
* Create and send an HTTP PATCH request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function patch(array $options = []): ResponseInterface;
/**
* Create and send an HTTP DELETE request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function delete(array $options = []): ResponseInterface;
/**
* Create and send an asynchronous HTTP GET request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function getAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP PUT request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function putAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP POST request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function postAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP PATCH request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function patchAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP DELETE request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function deleteAsync(array $options = []): PromiseInterface;
}
<?php declare(strict_types=1);
namespace WeChatPay;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Chainable points the client interface for sending HTTP requests.
*/
trait BuilderTrait
{
abstract public function getDriver(): ClientDecoratorInterface;
/**
* URI pathname
*
* @param string $seperator - The URI seperator, default is slash(`/`) character
*
* @return string - The URI string
*/
abstract protected function pathname(string $seperator = '/'): string;
/**
* @inheritDoc
*/
public function get(array $options = []): ResponseInterface
{
return $this->getDriver()->request('GET', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function put(array $options = []): ResponseInterface
{
return $this->getDriver()->request('PUT', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function post(array $options = []): ResponseInterface
{
return $this->getDriver()->request('POST', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function patch(array $options = []): ResponseInterface
{
return $this->getDriver()->request('PATCH', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function delete(array $options = []): ResponseInterface
{
return $this->getDriver()->request('DELETE', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function getAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('GET', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function putAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('PUT', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function postAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('POST', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function patchAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('PATCH', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function deleteAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('DELETE', $this->pathname(), $options);
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use function array_replace_recursive;
use function call_user_func;
use function sprintf;
use function php_uname;
use function implode;
use function strncasecmp;
use function strcasecmp;
use function substr;
use function constant;
use function defined;
use const PHP_OS;
use const PHP_VERSION;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\UriTemplate\UriTemplate;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Decorate the `GuzzleHttp\Client` instance
*/
final class ClientDecorator implements ClientDecoratorInterface
{
use ClientXmlTrait;
use ClientJsonTrait;
/**
* @var ClientInterface - The APIv2's `\GuzzleHttp\Client`
*/
protected $v2;
/**
* @var ClientInterface - The APIv3's `\GuzzleHttp\Client`
*/
protected $v3;
/**
* Deep merge the input with the defaults
*
* @param array<string,string|int|bool|array|mixed> $config - The configuration.
*
* @return array<string, string|mixed> - With the built-in configuration.
*/
protected static function withDefaults(array ...$config): array
{
return array_replace_recursive(static::$defaults, ['headers' => static::userAgent()], ...$config);
}
/**
* Prepare the `User-Agent` value key/value pair
*
* @return array<string, string>
*/
protected static function userAgent(): array
{
return ['User-Agent' => implode(' ', [
sprintf('wechatpay-php/%s', static::VERSION),
sprintf('GuzzleHttp/%s', constant(ClientInterface::class . (defined(ClientInterface::class . '::VERSION') ? '::VERSION' : '::MAJOR_VERSION'))),
sprintf('curl/%s', ((array)call_user_func('\curl_version'))['version'] ?? 'unknown'),
sprintf('(%s/%s)', PHP_OS, php_uname('r')),
sprintf('PHP/%s', PHP_VERSION),
])];
}
/**
* Taken body string
*
* @param MessageInterface $message - The message
*/
protected static function body(MessageInterface $message): string
{
$stream = $message->getBody();
$content = (string) $stream;
$stream->tell() && $stream->rewind();
return $content;
}
/**
* Decorate the `GuzzleHttp\Client` factory
*
* Acceptable \$config parameters stucture
* - mchid: string - The merchant ID
* - serial: string - The serial number of the merchant certificate
* - privateKey: \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string - The merchant private key.
* - certs: array<string, \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string> - The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
* - secret?: string - The secret key string (optional)
* - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
* - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
* - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
*
* @param array<string,string|int|bool|array|mixed> $config - `\GuzzleHttp\Client`, `APIv3` and `APIv2` configuration settings.
*/
public function __construct(array $config = [])
{
$this->{static::XML_BASED} = static::xmlBased($config);
$this->{static::JSON_BASED} = static::jsonBased($config);
}
/**
* Identify the `protocol` and `uri`
*
* @param string $uri - The uri string.
*
* @return string[] - the first element is the API version aka `protocol`, the second is the real `uri`
*/
private static function prepare(string $uri): array
{
return $uri && 0 === strncasecmp(static::XML_BASED . '/', $uri, 3)
? [static::XML_BASED, substr($uri, 3)]
: [static::JSON_BASED, $uri];
}
/**
* @inheritDoc
*/
public function select(?string $protocol = null): ClientInterface
{
return $protocol && 0 === strcasecmp(static::XML_BASED, $protocol)
? $this->{static::XML_BASED}
: $this->{static::JSON_BASED};
}
/**
* @inheritDoc
*/
public function request(string $method, string $uri, array $options = []): ResponseInterface
{
[$protocol, $pathname] = self::prepare(UriTemplate::expand($uri, $options));
return $this->select($protocol)->request($method, $pathname, $options);
}
/**
* @inheritDoc
*/
public function requestAsync(string $method, string $uri, array $options = []): PromiseInterface
{
[$protocol, $pathname] = self::prepare(UriTemplate::expand($uri, $options));
return $this->select($protocol)->requestAsync($method, $pathname, $options);
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Decorate the `GuzzleHttp\Client` interface
*/
interface ClientDecoratorInterface
{
/**
* @var string - This library version
*/
public const VERSION = '1.4.12';
/**
* @var string - The HTTP transfer `xml` based protocol
* @deprecated 1.0 - @see \WeChatPay\Exception\WeChatPayException::DEP_XML_PROTOCOL_IS_REACHABLE_EOL
*/
public const XML_BASED = 'v2';
/**
* @var string - The HTTP transfer `json` based protocol
*/
public const JSON_BASED = 'v3';
/**
* Protocol selector
*
* @param string|null $protocol - one of the constants of `XML_BASED`, `JSON_BASED`, default is `JSON_BASED`
* @return ClientInterface
*/
public function select(?string $protocol = null): ClientInterface;
/**
* Request the remote `$uri` by a HTTP `$method` verb
*
* @param string $uri - The uri string.
* @param string $method - The method string.
* @param array<string,string|int|bool|array|mixed> $options - The options.
*
* @return ResponseInterface - The `Psr\Http\Message\ResponseInterface` instance
*/
public function request(string $method, string $uri, array $options = []): ResponseInterface;
/**
* Async request the remote `$uri` by a HTTP `$method` verb
*
* @param string $uri - The uri string.
* @param string $method - The method string.
* @param array<string,string|int|bool|array|mixed> $options - The options.
*
* @return PromiseInterface - The `GuzzleHttp\Promise\PromiseInterface` instance
*/
public function requestAsync(string $method, string $uri, array $options = []): PromiseInterface;
}
<?php declare(strict_types=1);
namespace WeChatPay;
use function abs;
use function intval;
use function is_string;
use function is_resource;
use function is_object;
use function is_array;
use function implode;
use function count;
use function sprintf;
use function array_key_exists;
use function array_keys;
use function strcasecmp;
use function strncasecmp;
use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\MessageInterface;
/** @var int - The maximum clock offset in second */
const MAXIMUM_CLOCK_OFFSET = 300;
const WechatpayNonce = 'Wechatpay-Nonce';
const WechatpaySerial = 'Wechatpay-Serial';
const WechatpaySignature = 'Wechatpay-Signature';
const WechatpayTimestamp = 'Wechatpay-Timestamp';
const WechatpayStatementSha1 = 'Wechatpay-Statement-Sha1';
/**
* JSON based Client interface for sending HTTP requests.
*/
trait ClientJsonTrait
{
/**
* @var array<string, string|array<string, string>> - The defaults configuration whose pased in `GuzzleHttp\Client`.
*/
protected static $defaults = [
'base_uri' => 'https://api.mch.weixin.qq.com/',
'headers' => [
'Accept' => 'application/json, text/plain, application/x-gzip, application/pdf, image/png, image/*;q=0.5',
'Content-Type' => 'application/json; charset=utf-8',
],
];
abstract protected static function body(MessageInterface $message): string;
abstract protected static function withDefaults(array ...$config): array;
/**
* APIv3's signer middleware stack
*
* @param string $mchid - The merchant ID
* @param string $serial - The serial number of the merchant certificate
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string $privateKey - The merchant private key.
*
* @return callable(RequestInterface)
*/
public static function signer(
string $mchid,
string $serial,
#[\SensitiveParameter]
$privateKey
): callable
{
return static function (RequestInterface $request) use ($mchid, $serial, $privateKey): RequestInterface {
$nonce = Formatter::nonce();
$timestamp = (string) Formatter::timestamp();
$signature = Crypto\Rsa::sign(Formatter::request(
$request->getMethod(), $request->getRequestTarget(), $timestamp, $nonce, static::body($request)
), $privateKey);
return $request->withHeader('Authorization', Formatter::authorization(
$mchid, $nonce, $signature, $timestamp, $serial
));
};
}
/**
* Assert the HTTP `20X` responses fit for the business logic, otherwise thrown a `\GuzzleHttp\Exception\RequestException`.
*
* The `30X` responses were handled by `\GuzzleHttp\RedirectMiddleware`.
* The `4XX, 5XX` responses were handled by `\GuzzleHttp\Middleware::httpErrors`.
*
* @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string> $certs The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
* @return callable(ResponseInterface,RequestInterface)
* @throws RequestException
*/
protected static function assertSuccessfulResponse(array &$certs): callable
{
return static function (ResponseInterface $response, RequestInterface $request) use(&$certs): ResponseInterface {
if (
0 === strcasecmp($url = $request->getUri()->getPath(), '/v3/billdownload/file')
|| (0 === strncasecmp($url, '/v3/merchant-service/images/', 28) && 0 !== strcasecmp($url, '/v3/merchant-service/images/upload'))
) {
return $response;
}
if (!($response->hasHeader(WechatpayNonce) && $response->hasHeader(WechatpaySerial)
&& $response->hasHeader(WechatpaySignature) && $response->hasHeader(WechatpayTimestamp))) {
throw new RequestException(sprintf(
Exception\WeChatPayException::EV3_RES_HEADERS_INCOMPLETE,
WechatpayNonce, WechatpaySerial, WechatpaySignature, WechatpayTimestamp
), $request, $response);
}
[$nonce] = $response->getHeader(WechatpayNonce);
[$serial] = $response->getHeader(WechatpaySerial);
[$signature] = $response->getHeader(WechatpaySignature);
[$timestamp] = $response->getHeader(WechatpayTimestamp);
$localTimestamp = Formatter::timestamp();
if (abs($localTimestamp - intval($timestamp)) > MAXIMUM_CLOCK_OFFSET) {
throw new RequestException(sprintf(
Exception\WeChatPayException::EV3_RES_HEADER_TIMESTAMP_OFFSET,
MAXIMUM_CLOCK_OFFSET, $timestamp, $localTimestamp
), $request, $response);
}
if (!array_key_exists($serial, $certs)) {
throw new RequestException(sprintf(
Exception\WeChatPayException::EV3_RES_HEADER_PLATFORM_SERIAL,
$serial, WechatpaySerial, implode(',', array_keys($certs))
), $request, $response);
}
$isOverseas = (0 === strcasecmp($url, '/hk/v3/statements') || 0 === strcasecmp($url, '/v3/global/statements')) && $response->hasHeader(WechatpayStatementSha1);
$verified = false;
try {
$verified = Crypto\Rsa::verify(
Formatter::response(
$timestamp,
$nonce,
$isOverseas ? static::digestBody($response) : static::body($response)
),
$signature, $certs[$serial]
);
} catch (\Exception $exception) {}
if ($verified === false) {
throw new RequestException(sprintf(
Exception\WeChatPayException::EV3_RES_HEADER_SIGNATURE_DIGEST,
$timestamp, $nonce, $signature, $serial
), $request, $response, $exception ?? null);
}
return $response;
};
}
/**
* Downloading the reconciliation was required the client to format the `WechatpayStatementSha1` digest string as `JSON`.
*
* There was also sugguestion that to validate the response streaming's `SHA1` digest whether or nor equals to `WechatpayStatementSha1`.
* Here may contains with or without `gzip` parameter. Both of them are validating the plain `CSV` stream.
* Keep the same logic with the mainland's one(without `SHA1` validation).
* If someone needs this feature built-in, contrubiting is welcome.
*
* @see https://pay.weixin.qq.com/wiki/doc/api/wxpay/ch/fusion_wallet_ch/QuickPay/chapter8_5.shtml
* @see https://pay.weixin.qq.com/wiki/doc/api/wxpay/en/fusion_wallet/QuickPay/chapter8_5.shtml
* @see https://pay.weixin.qq.com/wiki/doc/api_external/ch/apis/chapter3_1_6.shtml
* @see https://pay.weixin.qq.com/wiki/doc/api_external/en/apis/chapter3_1_6.shtml
*
* @param ResponseInterface $response - The response instance
*
* @return string - The JSON string
*/
protected static function digestBody(ResponseInterface $response): string
{
return sprintf('{"sha1":"%s"}', $response->getHeader(WechatpayStatementSha1)[0]);
}
/**
* APIv3's verifier middleware stack
*
* @param array<string,\OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string> $certs The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
* @return callable(callable(RequestInterface,array))
*/
public static function verifier(array &$certs): callable
{
$assert = static::assertSuccessfulResponse($certs);
return static function (callable $handler) use ($assert): callable {
return static function (RequestInterface $request, array $options = []) use ($assert, $handler): PromiseInterface {
return $handler($request, $options)->then(static function(ResponseInterface $response) use ($assert, $request): ResponseInterface {
return $assert($response, $request);
});
};
};
}
/**
* Create an APIv3's client
*
* Mandatory \$config array paramters
* - mchid: string - The merchant ID
* - serial: string - The serial number of the merchant certificate
* - privateKey: \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string - The merchant private key.
* - certs: array{string, \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string} - The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
*
* @param array<string,string|int|bool|array|mixed> $config - The configuration
* @throws \WeChatPay\Exception\InvalidArgumentException
*/
public static function jsonBased(array $config = []): Client
{
if (!(
isset($config['mchid']) && is_string($config['mchid'])
)) { throw new Exception\InvalidArgumentException(Exception\ERR_INIT_MCHID_IS_MANDATORY); }
if (!(
isset($config['serial']) && is_string($config['serial'])
)) { throw new Exception\InvalidArgumentException(Exception\ERR_INIT_SERIAL_IS_MANDATORY); }
if (!(
isset($config['privateKey']) && (is_string($config['privateKey']) || is_resource($config['privateKey']) || is_object($config['privateKey']))
)) { throw new Exception\InvalidArgumentException(Exception\ERR_INIT_PRIVATEKEY_IS_MANDATORY); }
if (!(
isset($config['certs']) && is_array($config['certs']) && count($config['certs'])
)) { throw new Exception\InvalidArgumentException(Exception\ERR_INIT_CERTS_IS_MANDATORY); }
if (array_key_exists($config['serial'], $config['certs'])) {
throw new Exception\InvalidArgumentException(sprintf(
Exception\ERR_INIT_CERTS_EXCLUDE_MCHSERIAL, implode(',', array_keys($config['certs'])), $config['serial']
));
}
/** @var HandlerStack $stack */
$stack = isset($config['handler']) && ($config['handler'] instanceof HandlerStack) ? (clone $config['handler']) : HandlerStack::create();
$stack->before('prepare_body', Middleware::mapRequest(static::signer((string)$config['mchid'], $config['serial'], $config['privateKey'])), 'signer');
$stack->before('http_errors', static::verifier($config['certs']), 'verifier');
$config['handler'] = $stack;
unset($config['mchid'], $config['serial'], $config['privateKey'], $config['certs'], $config['secret'], $config['merchant']);
return new Client(static::withDefaults($config));
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use function strlen;
use function sprintf;
use function in_array;
use function array_key_exists;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Query;
use GuzzleHttp\Psr7\Utils;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\MessageInterface;
/**
* XML based Client interface for sending HTTP requests.
*/
trait ClientXmlTrait
{
/**
* @var array<string, string> - The default headers whose passed in `GuzzleHttp\Client`.
*/
protected static $headers = [
'Accept' => 'text/xml, text/plain, application/x-gzip',
'Content-Type' => 'text/xml; charset=utf-8',
];
/**
* @var string[] - Special URLs whose were designed that none signature respond.
*/
protected static $noneSignatureRespond = [
'/mchrisk/querymchrisk',
'/mchrisk/setmchriskcallback',
'/mchrisk/syncmchriskresult',
'/mmpaymkttransfers/gethbinfo',
'/mmpaymkttransfers/gettransferinfo',
'/mmpaymkttransfers/pay_bank',
'/mmpaymkttransfers/promotion/paywwsptrans2pocket',
'/mmpaymkttransfers/promotion/querywwsptrans2pocket',
'/mmpaymkttransfers/promotion/transfers',
'/mmpaymkttransfers/query_bank',
'/mmpaymkttransfers/sendgroupredpack',
'/mmpaymkttransfers/sendminiprogramhb',
'/mmpaymkttransfers/sendredpack',
'/papay/entrustweb',
'/papay/h5entrustweb',
'/papay/partner/entrustweb',
'/papay/partner/h5entrustweb',
'/pay/downloadbill',
'/pay/downloadfundflow',
'/payitil/report',
'/risk/getpublickey',
'/risk/getviolation',
'/secapi/mch/submchmanage',
'/xdc/apiv2getsignkey/sign/getsignkey',
];
abstract protected static function body(MessageInterface $message): string;
abstract protected static function withDefaults(array ...$config): array;
/**
* APIv2's transformRequest, did the `datasign` and `array2xml` together
*
* @param ?string $mchid - The merchant ID
* @param string $secret - The secret key string (optional)
* @param array{cert?: ?string, key?: ?string} $merchant - The merchant private key and certificate array. (optional)
*
* @return callable(callable(RequestInterface, array))
* @throws \WeChatPay\Exception\InvalidArgumentException
*/
public static function transformRequest(
?string $mchid = null,
#[\SensitiveParameter]
string $secret = '',
?array $merchant = null
): callable
{
return static function (callable $handler) use ($mchid, $secret, $merchant): callable {
return static function (RequestInterface $request, array $options = []) use ($handler, $mchid, $secret, $merchant): PromiseInterface {
$methodIsGet = $request->getMethod() === 'GET';
if ($methodIsGet) {
$queryParams = Query::parse($request->getUri()->getQuery());
}
$data = $options['xml'] ?? ($queryParams ?? []);
if ($mchid && $mchid !== ($inputMchId = $data['mch_id'] ?? $data['mchid'] ?? $data['combine_mch_id'] ?? null)) {
throw new Exception\InvalidArgumentException(sprintf(Exception\EV2_REQ_XML_NOTMATCHED_MCHID, $inputMchId ?? '', $mchid));
}
$type = $data['sign_type'] ?? Crypto\Hash::ALGO_MD5;
isset($options['nonceless']) || $data['nonce_str'] = $data['nonce_str'] ?? Formatter::nonce();
$data['sign'] = Crypto\Hash::sign($type, Formatter::queryStringLike(Formatter::ksort($data)), $secret);
$modify = $methodIsGet ? ['query' => Query::build($data)] : ['body' => Transformer::toXml($data)];
// for security request, it was required the merchant's private_key and certificate
if (isset($options['security']) && true === $options['security']) {
$options['ssl_key'] = $merchant['key'] ?? null;
$options['cert'] = $merchant['cert'] ?? null;
}
unset($options['xml'], $options['nonceless'], $options['security']);
return $handler(Utils::modifyRequest($request, $modify), $options);
};
};
}
/**
* APIv2's transformResponse, doing the `xml2array` then `verify` the signature job only
*
* @param string $secret - The secret key string (optional)
*
* @return callable(callable(RequestInterface, array))
*/
public static function transformResponse(
#[\SensitiveParameter]
string $secret = ''
): callable
{
return static function (callable $handler) use ($secret): callable {
return static function (RequestInterface $request, array $options = []) use ($secret, $handler): PromiseInterface {
if (in_array($request->getUri()->getPath(), static::$noneSignatureRespond)) {
return $handler($request, $options);
}
return $handler($request, $options)->then(static function(ResponseInterface $response) use ($secret) {
$result = Transformer::toArray(static::body($response));
if (!(array_key_exists('return_code', $result) && Crypto\Hash::equals('SUCCESS', $result['return_code']))) {
return Create::rejectionFor($response);
}
if (array_key_exists('result_code', $result) && !Crypto\Hash::equals('SUCCESS', $result['result_code'])) {
return Create::rejectionFor($response);
}
/** @var ?string $sign */
$sign = $result['sign'] ?? null;
$type = $sign && strlen($sign) === 64 ? Crypto\Hash::ALGO_HMAC_SHA256 : Crypto\Hash::ALGO_MD5;
/** @var string $calc - calculated digest string, it's naver `null` here because of \$type known. */
$calc = Crypto\Hash::sign($type, Formatter::queryStringLike(Formatter::ksort($result)), $secret);
return Crypto\Hash::equals($calc, $sign) ? $response : Create::rejectionFor($response);
});
};
};
}
/**
* Create an APIv2's client
*
* @deprecated 1.0 - @see \WeChatPay\Exception\WeChatPayException::DEP_XML_PROTOCOL_IS_REACHABLE_EOL
*
* Optional acceptable \$config parameters
* - mchid?: ?string - The merchant ID
* - secret?: ?string - The secret key string
* - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
* - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
* - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
*
* @param array<string,string|int|bool|array|mixed> $config - The configuration
*/
public static function xmlBased(array $config = []): Client
{
/** @var HandlerStack $stack */
$stack = isset($config['handler']) && ($config['handler'] instanceof HandlerStack) ? (clone $config['handler']) : HandlerStack::create();
$stack->before('prepare_body', static::transformRequest($config['mchid'] ?? null, $config['secret'] ?? '', $config['merchant'] ?? []), 'transform_request');
$stack->before('http_errors', static::transformResponse($config['secret'] ?? ''), 'transform_response');
$config['handler'] = $stack;
unset($config['mchid'], $config['serial'], $config['privateKey'], $config['certs'], $config['secret'], $config['merchant']);
return new Client(static::withDefaults(['headers' => static::$headers], $config));
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use function openssl_encrypt;
use function base64_encode;
use function openssl_decrypt;
use function base64_decode;
use const OPENSSL_RAW_DATA;
use UnexpectedValueException;
/**
* Aes encrypt/decrypt using `aes-256-ecb` algorithm with pkcs7padding.
*/
class AesEcb implements AesInterface
{
/**
* @inheritDoc
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string
{
$ciphertext = openssl_encrypt($plaintext, static::ALGO_AES_256_ECB, $key, OPENSSL_RAW_DATA, $iv = '');
if (false === $ciphertext) {
throw new UnexpectedValueException('Encrypting the input $plaintext failed, please checking your $key and $iv whether or nor correct.');
}
return base64_encode($ciphertext);
}
/**
* @inheritDoc
*/
public static function decrypt(
#[\SensitiveParameter]
string $ciphertext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string
{
$plaintext = openssl_decrypt(base64_decode($ciphertext), static::ALGO_AES_256_ECB, $key, OPENSSL_RAW_DATA, $iv = '');
if (false === $plaintext) {
throw new UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $key and $iv whether or nor correct.');
}
return $plaintext;
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use function in_array;
use function openssl_get_cipher_methods;
use function openssl_encrypt;
use function base64_encode;
use function base64_decode;
use function substr;
use function strlen;
use function openssl_decrypt;
use const OPENSSL_RAW_DATA;
use RuntimeException;
use UnexpectedValueException;
/**
* Aes encrypt/decrypt using `aes-256-gcm` algorithm with additional authenticated data(`aad`).
*/
class AesGcm implements AesInterface
{
/**
* Detect the ext-openssl whether or nor including the `aes-256-gcm` algorithm
*
* @throws RuntimeException
*/
private static function preCondition(): void
{
if (!in_array(static::ALGO_AES_256_GCM, openssl_get_cipher_methods())) {
throw new RuntimeException('It looks like the ext-openssl extension missing the `aes-256-gcm` cipher method.');
}
}
/**
* Encrypts given data with given key, iv and aad, returns a base64 encoded string.
*
* @param string $plaintext - Text to encode.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
* @param string $aad - The additional authenticated data, maybe empty string.
*
* @return string - The base64-encoded ciphertext.
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
#[\SensitiveParameter]
string $key,
string $iv = '',
string $aad = ''
): string
{
self::preCondition();
$ciphertext = openssl_encrypt($plaintext, static::ALGO_AES_256_GCM, $key, OPENSSL_RAW_DATA, $iv, $tag, $aad, static::BLOCK_SIZE);
if (false === $ciphertext) {
throw new UnexpectedValueException('Encrypting the input $plaintext failed, please checking your $key and $iv whether or nor correct.');
}
return base64_encode($ciphertext . $tag);
}
/**
* Takes a base64 encoded string and decrypts it using a given key, iv and aad.
*
* @param string $ciphertext - The base64-encoded ciphertext.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
* @param string $aad - The additional authenticated data, maybe empty string.
*
* @return string - The utf-8 plaintext.
*/
public static function decrypt(
#[\SensitiveParameter]
string $ciphertext,
#[\SensitiveParameter]
string $key,
string $iv = '',
string $aad = ''
): string
{
self::preCondition();
$ciphertext = base64_decode($ciphertext);
$authTag = substr($ciphertext, $tailLength = 0 - static::BLOCK_SIZE);
$tagLength = strlen($authTag);
/* Manually checking the length of the tag, because the `openssl_decrypt` was mentioned there, it's the caller's responsibility. */
if ($tagLength > static::BLOCK_SIZE || ($tagLength < 12 && $tagLength !== 8 && $tagLength !== 4)) {
throw new RuntimeException('The inputs `$ciphertext` incomplete, the bytes length must be one of 16, 15, 14, 13, 12, 8 or 4.');
}
$plaintext = openssl_decrypt(substr($ciphertext, 0, $tailLength), static::ALGO_AES_256_GCM, $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
if (false === $plaintext) {
throw new UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $key and $iv whether or nor correct.');
}
return $plaintext;
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
/**
* Advanced Encryption Standard Interface
*/
interface AesInterface
{
/**
* Bytes Length of the AES block
*/
public const BLOCK_SIZE = 16;
/**
* Bytes length of the AES secret key.
*/
public const KEY_LENGTH_BYTE = 32;
/**
* Bytes Length of the authentication tag in AEAD cipher mode
* @deprecated 1.0 - As of the OpenSSL described, the `auth_tag` length may be one of 16, 15, 14, 13, 12, 8 or 4.
* Keep it only compatible for the samples on the official documentation.
*/
public const AUTH_TAG_LENGTH_BYTE = 16;
/**
* The `aes-256-gcm` algorithm string
*/
public const ALGO_AES_256_GCM = 'aes-256-gcm';
/**
* The `aes-256-ecb` algorithm string
*/
public const ALGO_AES_256_ECB = 'aes-256-ecb';
/**
* Encrypts given data with given key and iv, returns a base64 encoded string.
*
* @param string $plaintext - Text to encode.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
*
* @return string - The base64-encoded ciphertext.
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string;
/**
* Takes a base64 encoded string and decrypts it using a given key and iv.
*
* @param string $ciphertext - The base64-encoded ciphertext.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
*
* @return string - The utf-8 plaintext.
*/
public static function decrypt(
#[\SensitiveParameter]
string $ciphertext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string;
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use function is_null;
use function hash_equals;
use function hash_init;
use function hash_update;
use function hash_final;
use function array_key_exists;
use function strtoupper;
use const HASH_HMAC;
const ALGO_MD5 = 'MD5';
const ALGO_HMAC_SHA256 = 'HMAC-SHA256';
const ALGO_DICTONARIES = [ALGO_HMAC_SHA256 => 'hmac', ALGO_MD5 => 'md5'];
/**
* Crypto hash functions utils.
* [Specification]{@link https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3}
*/
class Hash
{
/** @var string - hashing `MD5` algorithm */
public const ALGO_MD5 = ALGO_MD5;
/** @var string - hashing `HMAC-SHA256` algorithm */
public const ALGO_HMAC_SHA256 = ALGO_HMAC_SHA256;
/**
* Calculate the input string with an optional secret `key` in MD5,
* when the `key` is Falsey, this method works as normal `MD5`.
*
* @param string $thing - The input string.
* @param string $key - The secret key string.
* @param boolean|int|string $agency - The secret **key** is from work.weixin.qq.com, default is `false`,
* placed with `true` or better of the `AgentId` value.
* [spec]{@link https://work.weixin.qq.com/api/doc/90000/90135/90281}
*
* @return string - The data signature
*/
public static function md5(
string $thing,
#[\SensitiveParameter]
string $key = '',
$agency = false
): string
{
$ctx = hash_init(ALGO_MD5);
hash_update($ctx, $thing) && $key && hash_update($ctx, $agency ? '&secret=' : '&key=') && hash_update($ctx, $key);
return hash_final($ctx);
}
/**
* Calculate the input string with a secret `key` as of `algorithm` string which is one of the 'sha256', 'sha512' etc.
*
* @param string $thing - The input string.
* @param string $key - The secret key string.
* @param string $algorithm - The algorithm string, default is `sha256`.
*
* @return string - The data signature
*/
public static function hmac(
string $thing,
#[\SensitiveParameter]
string $key,
string $algorithm = 'sha256'
): string
{
$ctx = hash_init($algorithm, HASH_HMAC, $key);
hash_update($ctx, $thing) && hash_update($ctx, '&key=') && hash_update($ctx, $key);
return hash_final($ctx);
}
/**
* Wrapping the builtins `hash_equals` function.
*
* @param string $known_string - The string of known length to compare against.
* @param ?string $user_string - The user-supplied string.
*
* @return bool - Returns true when the two are equal, false otherwise.
*/
public static function equals(
#[\SensitiveParameter]
string $known_string,
#[\SensitiveParameter]
?string $user_string = null
): bool
{
return is_null($user_string) ? false : hash_equals($known_string, $user_string);
}
/**
* Utils of the data signature calculation.
*
* @param string $type - The sign type, one of the `MD5` or `HMAC-SHA256`.
* @param string $data - The input data.
* @param string $key - The secret key string.
*
* @return ?string - The data signature in UPPERCASE.
*/
public static function sign(
string $type,
string $data,
#[\SensitiveParameter]
string $key
): ?string
{
return array_key_exists($type, ALGO_DICTONARIES) ? strtoupper(static::{ALGO_DICTONARIES[$type]}($data, $key)) : null;
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use const OPENSSL_ALGO_SHA256;
use const OPENSSL_PKCS1_OAEP_PADDING;
use const PHP_URL_SCHEME;
use function array_column;
use function array_combine;
use function array_keys;
use function base64_decode;
use function base64_encode;
use function gettype;
use function is_int;
use function is_string;
use function ltrim;
use function openssl_pkey_get_private;
use function openssl_pkey_get_public;
use function openssl_private_decrypt;
use function openssl_public_encrypt;
use function openssl_sign;
use function openssl_verify;
use function pack;
use function parse_url;
use function preg_match;
use function sprintf;
use function str_replace;
use function strlen;
use function strpos;
use function substr;
use function wordwrap;
use UnexpectedValueException;
/**
* RSA `PKEY` loader and encrypt/decrypt/sign/verify methods.
*/
class Rsa
{
/** @var string - Type string of the asymmetric key */
public const KEY_TYPE_PUBLIC = 'public';
/** @var string - Type string of the asymmetric key */
public const KEY_TYPE_PRIVATE = 'private';
private const LOCAL_FILE_PROTOCOL = 'file://';
private const PKEY_PEM_NEEDLE = ' KEY-';
private const PKEY_PEM_FORMAT = "-----BEGIN %1\$s KEY-----\n%2\$s\n-----END %1\$s KEY-----";
private const PKEY_PEM_FORMAT_PATTERN = '#-{5}BEGIN ((?:RSA )?(?:PUBLIC|PRIVATE)) KEY-{5}\r?\n([^-]+)\r?\n-{5}END \1 KEY-{5}#';
private const CHR_CR = "\r";
private const CHR_LF = "\n";
/** @var array<string,array{string,string,int}> - Supported loading rules */
private const RULES = [
'private.pkcs1' => [self::PKEY_PEM_FORMAT, 'RSA PRIVATE', 16],
'private.pkcs8' => [self::PKEY_PEM_FORMAT, 'PRIVATE', 16],
'public.pkcs1' => [self::PKEY_PEM_FORMAT, 'RSA PUBLIC', 15],
'public.spki' => [self::PKEY_PEM_FORMAT, 'PUBLIC', 14],
];
/**
* @var string - Equal to `sequence(oid(1.2.840.113549.1.1.1), null))`
* @link https://datatracker.ietf.org/doc/html/rfc3447#appendix-A.2
*/
private const ASN1_OID_RSAENCRYPTION = '300d06092a864886f70d0101010500';
private const ASN1_SEQUENCE = 48;
private const CHR_NUL = "\0";
private const CHR_ETX = "\3";
/**
* Translate the \$thing strlen from `X690` style to the `ASN.1` 128bit hexadecimal length string
*
* @param string $thing - The string
*
* @return string The `ASN.1` 128bit hexadecimal length string
*/
private static function encodeLength(string $thing): string
{
$num = strlen($thing);
if ($num <= 0x7F) {
return sprintf('%c', $num);
}
$tmp = ltrim(pack('N', $num), self::CHR_NUL);
return pack('Ca*', strlen($tmp) | 0x80, $tmp);
}
/**
* Convert the `PKCS#1` format RSA Public Key to `SPKI` format
*
* @param string $thing - The base64-encoded string, without evelope style
*
* @return string The `SPKI` style public key without evelope string
*/
public static function pkcs1ToSpki(string $thing): string
{
$raw = self::CHR_NUL . base64_decode($thing);
$new = pack('H*', self::ASN1_OID_RSAENCRYPTION) . self::CHR_ETX . self::encodeLength($raw) . $raw;
return base64_encode(pack('Ca*a*', self::ASN1_SEQUENCE, self::encodeLength($new), $new));
}
/**
* Sugar for loading input `privateKey` string, pure `base64-encoded-string` without LF and evelope.
*
* @param string $thing - The string in `PKCS#8` format.
* @return \OpenSSLAsymmetricKey|resource|mixed
* @throws UnexpectedValueException
*/
public static function fromPkcs8(
#[\SensitiveParameter]
string $thing
)
{
return static::from(sprintf('private.pkcs8://%s', $thing), static::KEY_TYPE_PRIVATE);
}
/**
* Sugar for loading input `privateKey/publicKey` string, pure `base64-encoded-string` without LF and evelope.
*
* @param string $thing - The string in `PKCS#1` format.
* @param string $type - Either `self::KEY_TYPE_PUBLIC` or `self::KEY_TYPE_PRIVATE` string, default is `self::KEY_TYPE_PRIVATE`.
* @return \OpenSSLAsymmetricKey|resource|mixed
* @throws UnexpectedValueException
*/
public static function fromPkcs1(
#[\SensitiveParameter]
string $thing,
string $type = self::KEY_TYPE_PRIVATE
)
{
return static::from(sprintf('%s://%s', $type === static::KEY_TYPE_PUBLIC ? 'public.pkcs1' : 'private.pkcs1', $thing), $type);
}
/**
* Sugar for loading input `publicKey` string, pure `base64-encoded-string` without LF and evelope.
*
* @param string $thing - The string in `SKPI` format.
* @return \OpenSSLAsymmetricKey|resource|mixed
* @throws UnexpectedValueException
*/
public static function fromSpki(string $thing)
{
return static::from(sprintf('public.spki://%s', $thing), static::KEY_TYPE_PUBLIC);
}
/**
* Loading the privateKey/publicKey.
*
* The `\$thing` can be one of the following:
* - `file://` protocol `PKCS#1/PKCS#8 privateKey`/`SPKI publicKey`/`x509 certificate(for publicKey)` string.
* - `public.spki://`, `public.pkcs1://`, `private.pkcs1://`, `private.pkcs8://` protocols string.
* - full `PEM` in `PKCS#1/PKCS#8` format `privateKey`/`publicKey`/`x509 certificate(for publicKey)` string.
* - `\OpenSSLAsymmetricKey` (PHP8) or `resource#pkey` (PHP7).
* - `\OpenSSLCertificate` (PHP8) or `resource#X509` (PHP7) for publicKey.
* - `Array` of `[privateKeyString,passphrase]` for encrypted privateKey.
*
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|array{string,string}|string|mixed $thing - The thing.
* @param string $type - Either `self::KEY_TYPE_PUBLIC` or `self::KEY_TYPE_PRIVATE` string, default is `self::KEY_TYPE_PRIVATE`.
*
* @return \OpenSSLAsymmetricKey|resource|mixed
* @throws UnexpectedValueException
*/
public static function from(
#[\SensitiveParameter]
$thing,
string $type = self::KEY_TYPE_PRIVATE
)
{
$pkey = ($isPublic = $type === static::KEY_TYPE_PUBLIC)
? openssl_pkey_get_public(self::parse($thing, $type))
: openssl_pkey_get_private(self::parse($thing));
if (false === $pkey) {
throw new UnexpectedValueException(sprintf(
'Cannot load %s from(%s), please take care about the \$thing input.',
$isPublic ? 'publicKey' : 'privateKey',
gettype($thing)
));
}
return $pkey;
}
/**
* Parse the `\$thing` for the `openssl_pkey_get_public`/`openssl_pkey_get_private` function.
*
* The `\$thing` can be the `file://` protocol privateKey/publicKey string, eg:
* - `file:///my/path/to/private.pkcs1.key`
* - `file:///my/path/to/private.pkcs8.key`
* - `file:///my/path/to/public.spki.pem`
* - `file:///my/path/to/x509.crt` (for publicKey)
*
* The `\$thing` can be the `public.spki://`, `public.pkcs1://`, `private.pkcs1://`, `private.pkcs8://` protocols string, eg:
* - `public.spki://MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCg...`
* - `public.pkcs1://MIIBCgKCAQEAgYxTW5Yj...`
* - `private.pkcs1://MIIEpAIBAAKCAQEApdXuft3as2x...`
* - `private.pkcs8://MIIEpAIBAAKCAQEApdXuft3as2x...`
*
* The `\$thing` can be the string with PEM `evelope`, eg:
* - `-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----`
* - `-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----`
* - `-----BEGIN RSA PUBLIC KEY-----...-----END RSA PUBLIC KEY-----`
* - `-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----`
* - `-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----` (for publicKey)
*
* The `\$thing` can be the \OpenSSLAsymmetricKey/\OpenSSLCertificate/resouce, eg:
* - `\OpenSSLAsymmetricKey` (PHP8) or `resource#pkey` (PHP7) for publicKey/privateKey.
* - `\OpenSSLCertificate` (PHP8) or `resource#X509` (PHP7) for publicKey.
*
* The `\$thing` can be the Array{$privateKey,$passphrase} style for loading privateKey, eg:
* - [`file:///my/path/to/encrypted.private.pkcs8.key`, 'your_pass_phrase']
* - [`-----BEGIN ENCRYPTED PRIVATE KEY-----...-----END ENCRYPTED PRIVATE KEY-----`, 'your_pass_phrase']
*
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|array{string,string}|string|mixed $thing - The thing.
* @param string $type - Either `self::KEY_TYPE_PUBLIC` or `self::KEY_TYPE_PRIVATE` string, default is `self::KEY_TYPE_PRIVATE`.
* @return \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|array{string,string}|string|mixed
*/
private static function parse(
#[\SensitiveParameter]
$thing,
string $type = self::KEY_TYPE_PRIVATE
)
{
$src = $thing;
if (is_string($src) && is_int(strpos($src, self::PKEY_PEM_NEEDLE))
&& $type === static::KEY_TYPE_PUBLIC && preg_match(self::PKEY_PEM_FORMAT_PATTERN, $src, $matches)) {
[, $kind, $base64] = $matches;
$mapRules = (array)array_combine(array_column(self::RULES, 1/*column*/), array_keys(self::RULES));
$protocol = $mapRules[$kind] ?? '';
if ('public.pkcs1' === $protocol) {
$src = sprintf('%s://%s', $protocol, str_replace([self::CHR_CR, self::CHR_LF], '', $base64));
}
}
if (is_string($src) && is_bool(strpos($src, self::LOCAL_FILE_PROTOCOL)) && is_int(strpos($src, '://'))) {
$protocol = parse_url($src, PHP_URL_SCHEME);
[$format, $kind, $offset] = self::RULES[$protocol] ?? [null, null, null];
if ($format && $kind && $offset) {
$src = substr($src, $offset);
if ('public.pkcs1' === $protocol) {
$src = static::pkcs1ToSpki($src);
[$format, $kind] = self::RULES['public.spki'];
}
return sprintf($format, $kind, wordwrap($src, 64, self::CHR_LF, true));
}
}
return $src;
}
/**
* Check the padding mode whether or nor supported.
*
* @param int $padding - The padding mode, only support `OPENSSL_PKCS1_PADDING`, otherwise thrown `\UnexpectedValueException`.
*
* @throws UnexpectedValueException
*/
private static function paddingModeLimitedCheck(int $padding): void
{
if ($padding !== OPENSSL_PKCS1_OAEP_PADDING) {
throw new UnexpectedValueException(sprintf('Here\'s only support the OPENSSL_PKCS1_OAEP_PADDING(4) mode, yours(%d).', $padding));
}
}
/**
* Encrypts text by the given `$publicKey` in the `$padding`(default is `OPENSSL_PKCS1_OAEP_PADDING`) mode.
*
* @param string $plaintext - Cleartext to encode.
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|string|mixed $publicKey - The public key.
* @param int $padding - default is `OPENSSL_PKCS1_OAEP_PADDING`.
*
* @return string - The base64-encoded ciphertext.
* @throws UnexpectedValueException
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
$publicKey,
int $padding = OPENSSL_PKCS1_OAEP_PADDING
): string
{
self::paddingModeLimitedCheck($padding);
if (!openssl_public_encrypt($plaintext, $encrypted, $publicKey, $padding)) {
throw new UnexpectedValueException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.');
}
return base64_encode($encrypted);
}
/**
* Verifying the `message` with given `signature` string that uses `OPENSSL_ALGO_SHA256`.
*
* @param string $message - Content will be `openssl_verify`.
* @param string $signature - The base64-encoded ciphertext.
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|string|mixed $publicKey - The public key.
*
* @return boolean - True is passed, false is failed.
* @throws UnexpectedValueException
*/
public static function verify(string $message, string $signature, $publicKey): bool
{
if (($result = openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256)) === false) {
throw new UnexpectedValueException('Verified the input $message failed, please checking your $publicKey whether or nor correct.');
}
return $result === 1;
}
/**
* Creates and returns a `base64_encode` string that uses `OPENSSL_ALGO_SHA256`.
*
* @param string $message - Content will be `openssl_sign`.
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|string|mixed $privateKey - The private key.
*
* @return string - The base64-encoded signature.
* @throws UnexpectedValueException
*/
public static function sign(
string $message,
#[\SensitiveParameter]
$privateKey
): string
{
if (!openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
throw new UnexpectedValueException('Signing the input $message failed, please checking your $privateKey whether or nor correct.');
}
return base64_encode($signature);
}
/**
* Decrypts base64 encoded string with `$privateKey` in the `$padding`(default is `OPENSSL_PKCS1_OAEP_PADDING`) mode.
*
* @param string $ciphertext - Was previously encrypted string using the corresponding public key.
* @param \OpenSSLAsymmetricKey|\OpenSSLCertificate|resource|string|array{string,string}|mixed $privateKey - The private key.
* @param int $padding - default is `OPENSSL_PKCS1_OAEP_PADDING`.
*
* @return string - The utf-8 plaintext.
* @throws UnexpectedValueException
*/
public static function decrypt(
string $ciphertext,
#[\SensitiveParameter]
$privateKey,
int $padding = OPENSSL_PKCS1_OAEP_PADDING
): string
{
self::paddingModeLimitedCheck($padding);
if (!openssl_private_decrypt(base64_decode($ciphertext), $decrypted, $privateKey, $padding)) {
throw new UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $privateKey whether or nor correct.');
}
return $decrypted;
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Exception;
use GuzzleHttp\Exception\GuzzleException;
class InvalidArgumentException extends \InvalidArgumentException implements WeChatPayException, GuzzleException
{
}
<?php declare(strict_types=1);
namespace WeChatPay\Exception;
const DEP_XML_PROTOCOL_IS_REACHABLE_EOL = 'New features are all in `APIv3`, there\'s no reason to continue use this kind client since v2.0.';
const ERR_INIT_MCHID_IS_MANDATORY = 'The merchant\' ID aka `mchid` is required, usually numerical.';
const ERR_INIT_SERIAL_IS_MANDATORY = 'The serial number of the merchant\'s certificate aka `serial` is required, usually hexadecial.';
const ERR_INIT_PRIVATEKEY_IS_MANDATORY = 'The merchant\'s private key aka `privateKey` is required, usual as pem format.';
const ERR_INIT_CERTS_IS_MANDATORY = 'The platform certificate(s) aka `certs` is required, paired as of `[$serial => $certificate]`.';
const ERR_INIT_CERTS_EXCLUDE_MCHSERIAL = 'The `certs(%1$s)` contains the merchant\'s certificate serial number(%2$s) which is not allowed here.';
const EV2_REQ_XML_NOTMATCHED_MCHID = 'The xml\'s structure[mch_id(%1$s)] doesn\'t matched the init one mchid(%2$s).';
const EV3_RES_HEADERS_INCOMPLETE = 'The response\'s Headers incomplete, must have(`%1$s`, `%2$s`, `%3$s` and `%4$s`).';
const EV3_RES_HEADER_TIMESTAMP_OFFSET = 'It\'s allowed time offset in ± %1$s seconds, the response was on %2$s, your\'s localtime on %3$s.';
const EV3_RES_HEADER_PLATFORM_SERIAL = 'Cannot found the serial(`%1$s`)\'s configuration, which\'s from the response(header:%2$s), your\'s %3$s.';
const EV3_RES_HEADER_SIGNATURE_DIGEST = 'Verify the response\'s data with: timestamp=%1$s, nonce=%2$s, signature=%3$s, cert=[%4$s => ...] failed.';
interface WeChatPayException
{
const DEP_XML_PROTOCOL_IS_REACHABLE_EOL = DEP_XML_PROTOCOL_IS_REACHABLE_EOL;
const EV3_RES_HEADERS_INCOMPLETE = EV3_RES_HEADERS_INCOMPLETE;
const EV3_RES_HEADER_TIMESTAMP_OFFSET = EV3_RES_HEADER_TIMESTAMP_OFFSET;
const EV3_RES_HEADER_PLATFORM_SERIAL = EV3_RES_HEADER_PLATFORM_SERIAL;
const EV3_RES_HEADER_SIGNATURE_DIGEST = EV3_RES_HEADER_SIGNATURE_DIGEST;
}
<?php declare(strict_types=1);
namespace WeChatPay;
use function str_split;
use function array_map;
use function ord;
use function random_bytes;
use function time;
use function sprintf;
use function implode;
use function array_merge;
use function ksort;
use function is_null;
use const SORT_STRING;
use InvalidArgumentException;
/**
* Provides easy used methods using in this project.
*/
class Formatter
{
/**
* Generate a random BASE62 string aka `nonce`, similar as `random_bytes`.
*
* @param int $size - Nonce string length, default is 32.
*
* @return string - base62 random string.
*/
public static function nonce(int $size = 32): string
{
if ($size < 1) {
throw new InvalidArgumentException('Size must be a positive integer.');
}
return implode('', array_map(static function(string $c): string {
return '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'[ord($c) % 62];
}, str_split(random_bytes($size))));
}
/**
* Retrieve the current `Unix` timestamp.
*
* @return int - Epoch timestamp.
*/
public static function timestamp(): int
{
return time();
}
/**
* Formatting for the heading `Authorization` value.
*
* @param string $mchid - The merchant ID.
* @param string $nonce - The Nonce string.
* @param string $signature - The base64-encoded `Rsa::sign` ciphertext.
* @param string $timestamp - The `Unix` timestamp.
* @param string $serial - The serial number of the merchant public certification.
*
* @return string - The APIv3 Authorization `header` value
*/
public static function authorization(string $mchid, string $nonce, string $signature, string $timestamp, string $serial): string
{
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",serial_no="%s",timestamp="%s",nonce_str="%s",signature="%s"',
$mchid, $serial, $timestamp, $nonce, $signature
);
}
/**
* Formatting this `HTTP::request` for `Rsa::sign` input.
*
* @param string $method - The HTTP verb, must be the uppercase sting.
* @param string $uri - Combined string with `URL::pathname` and `URL::search`.
* @param string $timestamp - The `Unix` timestamp, should be the one used in `authorization`.
* @param string $nonce - The `Nonce` string, should be the one used in `authorization`.
* @param string $body - The playload string, HTTP `GET` should be an empty string.
*
* @return string - The content for `Rsa::sign`
*/
public static function request(string $method, string $uri, string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($method, $uri, $timestamp, $nonce, $body);
}
/**
* Formatting this `HTTP::response` for `Rsa::verify` input.
*
* @param string $timestamp - The `Unix` timestamp, should be the one from `response::headers[Wechatpay-Timestamp]`.
* @param string $nonce - The `Nonce` string, should be the one from `response::headers[Wechatpay-Nonce]`.
* @param string $body - The response payload string, HTTP status(`201`, `204`) should be an empty string.
*
* @return string - The content for `Rsa::verify`
*/
public static function response(string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($timestamp, $nonce, $body);
}
/**
* Joined this inputs by for `Line Feed`(LF) char.
*
* @param string|float|int|bool $pieces - The scalar variable(s).
*
* @return string - The joined string.
*/
public static function joinedByLineFeed(...$pieces): string
{
return implode("\n", array_merge($pieces, ['']));
}
/**
* Sort an array by key with `SORT_STRING` flag.
*
* @param array<string, string|int> $thing - The input array.
*
* @return array<string, string|int> - The sorted array.
*/
public static function ksort(array $thing = []): array
{
ksort($thing, SORT_STRING);
return $thing;
}
/**
* Like `queryString` does but without the `sign` and `empty value` entities.
*
* @param array<string, string|int|null> $thing - The input array.
*
* @return string - The `key=value` pair string whose joined by `&` char.
*/
public static function queryStringLike(array $thing = []): string
{
$data = [];
foreach ($thing as $key => $value) {
if ($key === 'sign' || is_null($value) || $value === '') {
continue;
}
$data[] = implode('=', [$key, $value]);
}
return implode('&', $data);
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use const LIBXML_VERSION;
use const LIBXML_NONET;
use const LIBXML_COMPACT;
use const LIBXML_NOCDATA;
use const LIBXML_NOBLANKS;
use function array_walk;
use function is_array;
use function is_object;
use function is_string;
use function preg_replace;
use function strpos;
use function preg_match;
use function sprintf;
use function trigger_error;
use function libxml_clear_errors;
use function libxml_disable_entity_loader;
use function libxml_get_last_error;
use function libxml_use_internal_errors;
use function simplexml_load_string;
use SimpleXMLElement;
use Traversable;
use XMLWriter;
/**
* Transform the `XML` to `Array` or `Array` to `XML`.
*/
class Transformer
{
/**
* Convert the $xml string to array.
*
* Always issue the `additional Libxml parameters` asof `LIBXML_NONET`
* | `LIBXML_COMPACT`
* | `LIBXML_NOCDATA`
* | `LIBXML_NOBLANKS`
*
* @param string $xml - The xml string, default is `<xml/>` string
*
* @return array<string,string|array|mixed>
*/
public static function toArray(string $xml = '<xml/>'): array
{
LIBXML_VERSION < 20900 && $previous = libxml_disable_entity_loader(true);
libxml_use_internal_errors(true);
$el = simplexml_load_string(static::sanitize($xml), SimpleXMLElement::class, LIBXML_NONET | LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS);
LIBXML_VERSION < 20900 && isset($previous) && libxml_disable_entity_loader($previous);
if (false === $el) {
// while parsing failed, let's clean the internal buffer and
// only leave the last error message which still can be fetched by the `error_get_last()` function.
if (false !== ($err = libxml_get_last_error())) {
libxml_clear_errors();
@trigger_error(sprintf(
'Parsing the $xml failed with the last error(level=%d,code=%d,message=%s).',
$err->level, $err->code, $err->message
));
}
return [];
}
return static::cast($el);
}
/**
* Recursive cast the $thing as array data structure.
*
* @param array<string,mixed>|object|\SimpleXMLElement $thing - The thing
*
* @return array<string,string|array|mixed>
*/
protected static function cast($thing): array
{
$data = (array) $thing;
array_walk($data, static function(&$value) { static::value($value); });
return $data;
}
/**
* Cast the value $thing, specially doing the `array`, `object`, `SimpleXMLElement` to `array`
*
* @param string|array<string,string|\SimpleXMLElement|mixed>|object|\SimpleXMLElement $thing - The value thing reference
*/
protected static function value(&$thing): void
{
is_array($thing) && $thing = static::cast($thing);
if (is_object($thing) && $thing instanceof SimpleXMLElement) {
$thing = $thing->count() ? static::cast($thing) : (string) $thing;
}
}
/**
* Trim invalid characters from the $xml string
*
* @see https://github.com/w7corp/easywechat/pull/1419
* @license https://github.com/w7corp/easywechat/blob/4.x/LICENSE
*
* @param string $xml - The xml string
*/
public static function sanitize(string $xml): string
{
return preg_replace('#[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+#u', '', $xml) ?? '';
}
/**
* Transform the given $data array as of an XML string.
*
* @param array<string,string|array|mixed> $data - The data array
* @param boolean $headless - The headless flag, default `true` means without the `<?xml version="1.0" encoding="UTF-8" ?>` doctype
* @param boolean $indent - Toggle indentation on/off, default is `false` off
* @param string $root - The root node label, default is `xml` string
* @param string $item - The nest array identify text, default is `item` string
*
* @return string - The xml string
*/
public static function toXml(array $data, bool $headless = true, bool $indent = false, string $root = 'xml', string $item = 'item'): string
{
$writer = new XMLWriter();
$writer->openMemory();
$writer->setIndent($indent);
$headless || $writer->startDocument('1.0', 'utf-8');
$writer->startElement($root);
static::walk($writer, $data, $item);
$writer->endElement();
$headless || $writer->endDocument();
$xml = $writer->outputMemory();
$writer = null;
return $xml;
}
/**
* Walk the given data array by the `XMLWriter` instance.
*
* @param \XMLWriter $writer - The `XMLWriter` instance reference
* @param array<string,string|array|mixed> $data - The data array
* @param string $item - The nest array identify tag text
*/
protected static function walk(XMLWriter &$writer, array $data, string $item): void
{
foreach ($data as $key => $value) {
$tag = is_string($key) && static::isElementNameValid($key) ? $key : $item;
$writer->startElement($tag);
if (is_array($value) || (is_object($value) && $value instanceof Traversable)) {
static::walk($writer, (array) $value, $item);
} else {
static::content($writer, (string) $value);
}
$writer->endElement();
}
}
/**
* Write content text.
*
* The content text includes the characters `<`, `>`, `&` and `"` are written as CDATA references.
* All others including `'` are written literally.
*
* @param \XMLWriter $writer - The `XMLWriter` instance reference
* @param string $thing - The content text
*/
protected static function content(XMLWriter &$writer, string $thing = ''): void
{
static::needsCdataWrapping($thing) && $writer->writeCdata($thing) || $writer->text($thing);
}
/**
* Checks the name is a valid xml element name.
*
* @see \Symfony\Component\Serializer\Encoder\XmlEncoder::isElementNameValid
* @license https://github.com/symfony/serializer/blob/5.3/LICENSE
*
* @param string $name - The name
*
* @return boolean - True means valid
*/
protected static function isElementNameValid(string $name = ''): bool
{
return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._:-]*$#ui', $name);
}
/**
* Checks if a value contains any characters which would require CDATA wrapping.
*
* Notes here: the `XMLWriter` shall been wrapped the `"` string as `&quot;` symbol string,
* it's strictly following the `XMLWriter` specification here.
*
* @see \Symfony\Component\Serializer\Encoder\XmlEncoder::needsCdataWrapping
* @license https://github.com/symfony/serializer/blob/5.3/LICENSE
*
* @param string $value - The value
*
* @return boolean - True means need
*/
protected static function needsCdataWrapping(string $value = ''): bool
{
return $value && 0 < preg_match('#[>&"<]#', $value);
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Util;
use function basename;
use function sprintf;
use function json_encode;
use UnexpectedValueException;
use GuzzleHttp\Psr7\Utils;
use GuzzleHttp\Psr7\BufferStream;
use GuzzleHttp\Psr7\LazyOpenStream;
use GuzzleHttp\Psr7\MultipartStream;
use GuzzleHttp\Psr7\FnStream;
use GuzzleHttp\Psr7\CachingStream;
use Psr\Http\Message\StreamInterface;
/**
* Util for Media(image, video or text/csv whose are the platform acceptable file types etc) uploading.
*/
class MediaUtil
{
/**
* @var string - local file path
*/
private $filepath;
/**
* @var ?StreamInterface - The `file` stream
*/
private $fileStream;
/**
* @var StreamInterface - The `meta` stream
*/
private $metaStream;
/**
* @var MultipartStream - The `multipart/form-data` stream
*/
private $multipart;
/**
* @var StreamInterface - multipart stream wrapper
*/
private $stream;
/**
* Constructor
*
* @param string $filepath The media file path or file name,
* should be one of the
* images(jpg|bmp|png)
* or
* video(avi|wmv|mpeg|mp4|mov|mkv|flv|f4v|m4v|rmvb)
* or
* text/csv whose are the platform acceptable etc.
* @param ?StreamInterface $fileStream File content stream, optional
*/
public function __construct(string $filepath, ?StreamInterface $fileStream = null)
{
$this->filepath = $filepath;
$this->fileStream = $fileStream;
$this->composeStream();
}
/**
* Compose the GuzzleHttp\Psr7\FnStream
*/
private function composeStream(): void
{
$basename = basename($this->filepath);
$stream = $this->fileStream ?? new LazyOpenStream($this->filepath, 'rb');
if ($stream instanceof StreamInterface && !($stream->isSeekable())) {
$stream = new CachingStream($stream);
}
if (!($stream instanceof StreamInterface)) {
throw new UnexpectedValueException(sprintf('Cannot open or caching the file: `%s`', $this->filepath));
}
$buffer = new BufferStream();
$metaStream = FnStream::decorate($buffer, [
'getSize' => static function () { return null; },
// The `BufferStream` doen't have `uri` metadata(`null` returned),
// but the `MultipartStream` did checked this prop with the `substr` method, which method described
// the first paramter must be the string on the `strict_types` mode.
// Decorate the `getMetadata` for this case.
'getMetadata' => static function($key = null) use ($buffer) {
if ('uri' === $key) { return 'php://temp'; }
return $buffer->getMetadata($key);
},
]);
$this->fileStream = $this->fileStream ?? $stream;
$this->metaStream = $metaStream;
$this->setMeta();
$multipart = new MultipartStream([
[
'name' => 'meta',
'contents' => $this->metaStream,
'headers' => [
'Content-Type' => 'application/json',
],
],
[
'name' => 'file',
'filename' => $basename,
'contents' => $this->fileStream,
],
]);
$this->multipart = $multipart;
$this->stream = FnStream::decorate($multipart, [
'__toString' => function () { return $this->getMeta(); },
'getSize' => static function () { return null; },
]);
}
/**
* Set the `meta` part of the `multipart/form-data` stream
*
* Note: The `meta` weren't be the `media file`'s `meta data` anymore.
*
* Previous whose were designed as `{filename,sha256}`,
* but another API was described asof `{bank_type,filename,sha256}`.
*
* Exposed the ability of setting the `meta` for the `new` data structure.
*
* @param ?string $json - The `meta` string
* @since v1.3.2
*/
public function setMeta(?string $json = null): int
{
$content = $json ?? (string)json_encode([
'filename' => basename($this->filepath),
'sha256' => $this->fileStream ? Utils::hash($this->fileStream, 'sha256') : '',
]);
// clean the metaStream's buffer string
$this->metaStream->getContents();
return $this->metaStream->write($content);
}
/**
* Get the `meta` string
*/
public function getMeta(): string
{
$json = (string)$this->metaStream;
$this->setMeta($json);
return $json;
}
/**
* Get the `FnStream` which is the `MultipartStream` decorator
*/
public function getStream(): StreamInterface
{
return $this->stream;
}
/**
* Get the `Content-Type` value from the `{$this->multipart}` instance
*/
public function getContentType(): string
{
return 'multipart/form-data; boundary=' . $this->multipart->getBoundary();
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Util;
use function openssl_x509_read;
use function openssl_x509_parse;
use function file_get_contents;
use function strtoupper;
use function strpos;
use UnexpectedValueException;
use WeChatPay\Crypto\Rsa;
/**
* Util for read private key and certificate.
*/
class PemUtil
{
private const LOCAL_FILE_PROTOCOL = 'file://';
/**
* Read private key from file
* @deprecated v1.2.0 - Use `Rsa::from` instead
*
* @param string $filepath - PEM encoded private key file path
*
* @return \OpenSSLAsymmetricKey|resource|mixed
*/
public static function loadPrivateKey(
#[\SensitiveParameter]
string $filepath
)
{
return Rsa::from((false === strpos($filepath, self::LOCAL_FILE_PROTOCOL) ? self::LOCAL_FILE_PROTOCOL : '') . $filepath);
}
/**
* Read private key from string
* @deprecated v1.2.0 - Use `Rsa::from` instead
*
* @param \OpenSSLAsymmetricKey|resource|string|mixed $content - PEM encoded private key string content
*
* @return \OpenSSLAsymmetricKey|resource|mixed
*/
public static function loadPrivateKeyFromString(
#[\SensitiveParameter]
$content
)
{
return Rsa::from($content);
}
/**
* Read certificate from file
*
* @param string $filepath - PEM encoded X.509 certificate file path
*
* @return \OpenSSLCertificate|object|resource|bool - X.509 certificate resource identifier on success or FALSE on failure
* @throws UnexpectedValueException
*/
public static function loadCertificate(string $filepath)
{
$content = file_get_contents($filepath);
if (false === $content) {
throw new UnexpectedValueException("Loading the certificate failed, please checking your {$filepath} input.");
}
return openssl_x509_read($content);
}
/**
* Read certificate from string
*
* @param \OpenSSLCertificate|object|resource|string|mixed $content - PEM encoded X.509 certificate string content
*
* @return \OpenSSLCertificate|object|resource|bool - X.509 certificate resource identifier on success or FALSE on failure
*/
public static function loadCertificateFromString($content)
{
return openssl_x509_read($content);
}
/**
* Parse Serial Number from Certificate
*
* @param \OpenSSLCertificate|object|resource|string|mixed $certificate Certificates string or resource
*
* @return string - The serial number
* @throws UnexpectedValueException
*/
public static function parseCertificateSerialNo($certificate): string
{
$info = openssl_x509_parse($certificate);
if (false === $info || !isset($info['serialNumberHex'])) {
throw new UnexpectedValueException('Read the $certificate failed, please check it whether or nor correct');
}
return strtoupper($info['serialNumberHex']);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment