Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 添加平台公钥加密功能及补充文档 #2872

Merged
merged 11 commits into from
Jan 7, 2025
52 changes: 45 additions & 7 deletions docs/src/6.x/pay/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

> 👏🏻 欢迎点击本页下方 "帮助我们改善此页面!" 链接参与贡献更多的使用示例!



<details>
<summary>JSAPI 下单</summary>

Expand All @@ -30,7 +28,6 @@ $response = $app->getClient()->postJson("v3/pay/transactions/jsapi", [

</details>


<details>
<summary>Native 下单</summary>

Expand All @@ -49,8 +46,8 @@ $response = $app->getClient()->postJson('v3/pay/transactions/native', [

print_r($response->toArray(false));
```
</details>

</details>

<details>
<summary>查询订单(商户订单号)</summary>
Expand All @@ -66,8 +63,8 @@ $response = $app->getClient()->get("v3/pay/transactions/out-trade-no/{$outTradeN

print_r($response->toArray());
```
</details>

</details>

<details>
<summary>查询订单(微信订单号)</summary>
Expand All @@ -82,6 +79,7 @@ $response = $app->getClient()->get("pay/transactions/id/{$transactionId}", [

print_r($response->toArray());
```

</details>

<details>
Expand Down Expand Up @@ -118,6 +116,7 @@ Route::post('payment_notify', function () {
return $server->serve();
});
```

</details>

<details>
Expand All @@ -141,13 +140,13 @@ $response = $api->post('/mmpaymkttransfers/promotion/transfers', [

print_r($response->toArray());
```
</details>

</details>

<details>
<summary>JSAPI下单(服务商)</summary>

> 官方文档:<[https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml](https://pay.weixin.qq.com/docs/partner/apis/partner-jsapi-payment/partner-jsons/partner-jsapi-prepay.html)>
> 官方文档:<[https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml](https://pay.weixin.qq.com/docs/partner/apis/partner-jsapi-payment/partner-jsons/partner-jsapi-prepay.html)>

```php
$response = $app->getClient()->postJson("v3/pay/partner/transactions/jsapi", [
Expand All @@ -170,6 +169,45 @@ print_r($response->toArray());

print_r($response->toArray());
```

</details>

<details>
<summary>敏感信息加密</summary>

> 官方文档:<https://pay.weixin.qq.com/doc/v3/merchant/4013053257>
> 使用默认公钥 ID

```php
$utils = $app->getUtils();
$response = $app->getClient()->withSerialHeader()->postJson("v3/applyment4sub/applyment/", [
"business_code" => "12345678",
'contact_info' => [
'contact_name' => $utils->createRsaEncrypt('张三'),
//...
],
//...
]);

print_r($response->toArray());
```

或指定公钥 ID

```php
$utils = $app->getUtils();
$response = $app->getClient()->withSerialHeader("PUB_KEY_ID_123456")->postJson("v3/applyment4sub/applyment/", [
"business_code" => "12345678",
'contact_info' => [
'contact_name' => $utils->createRsaEncrypt("张三","PUB_KEY_ID_123456"),
//...
],
//...
]);

print_r($response->toArray());
```

</details>

<!--
Expand Down
16 changes: 8 additions & 8 deletions docs/src/6.x/pay/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
请仔细阅读并理解:[微信官方文档 - 微信支付](https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/index.shtml)

> [!NOTE]
> 2024年Q3,微信支付官方开启了「平台公钥」平替「平台证书」方案,初始化所需的参数仅需配置上 **平台公钥ID** 及 **平台公钥** 即完全兼容支持,CLI/API下载 **平台证书** 已不是一个必要步骤,可略过。
> **平台公钥ID** 及 **平台公钥** 均可在 [微信支付商户平台](https://pay.weixin.qq.com/) -> 账户中心 -> API安全 查看及/或下载。
> 2024 年 Q3,微信支付官方开启了「微信支付公钥」平替「平台证书」方案,初始化所需的参数仅需配置上 **微信支付公钥 ID** 及 **微信支付公钥** 即完全兼容支持,CLI/API 下载 **平台证书** 已不是一个必要步骤,可略过。
> **微信支付公钥 ID** 及 **微信支付公钥** 均可在 [微信支付商户平台](https://pay.weixin.qq.com/) -> 账户中心 -> API 安全 查看及/或下载。

## 实例化 {#init}

Expand Down Expand Up @@ -32,8 +32,8 @@ $config = [
// 可简写使用平台证书文件绝对路径
// '/path/to/wechatpay/cert.pem',

// 如果是「平台公钥」模式
// 使用Key/Value结构, key为平台公钥ID,value为平台公钥文件绝对路径
// 如果是「微信支付公钥」模式
// 使用Key/Value结构, key为微信支付公钥ID,value为微信支付公钥文件绝对路径
// "{$pubKeyId}" => '/path/to/wechatpay/pubkey.pem',
],

Expand Down Expand Up @@ -96,6 +96,7 @@ $account->getPrivateKey();
$account->getCertificate();
$account->getSecretKey();
$account->getV2SecretKey();
$account->getPlatformCert($serial);
$account->getPlatformCerts();
```

Expand All @@ -119,22 +120,22 @@ $server = $app->getServer();
$server->handlePaid(function (Message $message, \Closure $next) use ($app) {
// $message->out_trade_no 获取商户订单号
// $message->payer['openid'] 获取支付者 openid

try{
$app->getValidator()->validate($app->getRequest());
// 验证通过,业务处理
} catch(Exception $e){
// 验证失败
}

return $next($message);
});

// 默认返回 ['code' => 'SUCCESS', 'message' => '成功']
return $server->serve();
```

##### API返回值的签名验证 {#verify-response}
##### API 返回值的签名验证 {#verify-response}

```php
// API 请求示例
Expand All @@ -153,4 +154,3 @@ try{
```bash
openssl x509 -in /path/to/merchant/apiclient_cert.pem -noout -serial | awk -F= '{print $2}'
```

87 changes: 55 additions & 32 deletions docs/src/6.x/pay/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,43 @@ $utils = $app->getUtils();

:book: [官方文档 - WeixinJSBridge 调起支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml)

```php
$appId = '商户申请的公众号对应的 appid,由微信支付生成,可在公众号后台查看';
$signType = 'RSA'; // 默认RSA,v2要传MD5
$config = $utils->buildBridgeConfig($prepayId, $appId, $signType); // 返回数组
```
```php
$appId = '商户申请的公众号对应的 appid,由微信支付生成,可在公众号后台查看';
$signType = 'RSA'; // 默认RSA,v2要传MD5
$config = $utils->buildBridgeConfig($prepayId, $appId, $signType); // 返回数组
```

调用示例

```js
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
```js
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
timeStamp: "<?= $config['timeStamp'] ?>", //注意 timeStamp 的格式
nonceStr: "<?= $config['nonceStr'] ?>",
package: "?= $config['package'] ?>",
signType: "<?= $config['signType'] ?>",
paySign: "<?= $config['paySign'] ?>", // 支付签名
paySign: "<?= $config['paySign'] ?>" // 支付签名
},
function (res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
if (res.err_msg == 'get_brand_wcpay_request:ok') {
// 使用以上方式判断前端返回,微信团队郑重提示:
// res.err_msg将在用户支付成功后返回
// ok,但并不保证它绝对可靠。
}
}
);
```
)
```

### JSSDK 调起支付 API

:book: [官方文档 - wx.chooseWXPay 调起支付](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#58)

```php
$appId = '商户申请的公众号对应的 appid,由微信支付生成,可在公众号后台查看';
$signType = 'RSA'; // 默认RSA,v2要传MD5
$config = $utils->buildSdkConfig($prepayId, $appId, $signType); // 返回数组
```
```php
$appId = '商户申请的公众号对应的 appid,由微信支付生成,可在公众号后台查看';
$signType = 'RSA'; // 默认RSA,v2要传MD5
$config = $utils->buildSdkConfig($prepayId, $appId, $signType); // 返回数组
```

调用实例:

Expand All @@ -74,23 +75,23 @@ wx.chooseWXPay({
success: function (res) {
// 支付成功后的回调函数
}
});
})
```

### 小程序调起支付 API

:book: [官方文档 - 小程序调起支付 API](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml)

```php
$appId = '商户申请的小程序对应的appid,由微信支付生成,可在小程序后台查看';
$signType = 'RSA'; // 默认RSA,v2要传MD5
$config = $utils->buildMiniAppConfig($prepayId, $appId, $signType); // 返回数组
```
```php
$appId = '商户申请的小程序对应的appid,由微信支付生成,可在小程序后台查看';
$signType = 'RSA'; // 默认RSA,v2要传MD5
$config = $utils->buildMiniAppConfig($prepayId, $appId, $signType); // 返回数组
```

调用示例:

```js
wx.requestPayment({
```js
wx.requestPayment({
timeStamp: "<?= $config['timeStamp'] ?>",
nonceStr: "<?= $config['nonceStr'] ?>",
package: "<?= $config['package'] ?>",
Expand All @@ -99,25 +100,47 @@ wx.chooseWXPay({
success: function (res) {
// 支付成功后的回调函数
}
});
```
})
```

### APP 调起支付 API

:book: [官方文档 - APP 调起支付 API](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_4.shtml)

```php
$appId = '商户申请的公众号对应的appid,由微信支付生成,可在公众号后台查看';
$config = $utils->buildAppConfig($prepayId, $appId); // 返回数组
```
```php
$appId = '商户申请的公众号对应的appid,由微信支付生成,可在公众号后台查看';
$config = $utils->buildAppConfig($prepayId, $appId); // 返回数组
```

调用示例:[官方文档 - APP 调起支付 API](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_4.shtml)

### 使用微信支付公钥加密敏感字段

:book: [官方文档 - 如何使用微信支付公钥加密敏感字段](https://pay.weixin.qq.com/doc/v3/merchant/4012153196)

```php
$config = [
'platform_certs' => [
// 如果是「平台证书」模式
// 可简写使用平台证书文件绝对路径
// '/path/to/wechatpay/cert.pem',

// 如果是「平台公钥」模式
// 使用Key/Value结构, key为平台公钥ID,value为平台公钥文件绝对路径
// "{$pubKeyId}" => '/path/to/wechatpay/pubkey.pem',
],
];
//使用微信支付公钥加密敏感字段可传入$serial(即 $pubKeyId),或不传默认取第一个证书
$encrypted = $utils->encryptWithRsaPublicKey($plaintext, $serial); // 返回加密后数据
```

调用示例:[官方文档 - 如何使用微信支付公钥加密敏感字段](https://pay.weixin.qq.com/doc/v3/merchant/4013053257)

# 二维码生成工具推荐

> :heart: 建议由前端生成二维码

确实需要用PHP生成二维码,那么以下这些供参考:
确实需要用 PHP 生成二维码,那么以下这些供参考:

- [endroid/QrCode](https://github.com/endroid/QrCode)
- [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode)
Expand Down
23 changes: 23 additions & 0 deletions src/Pay/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ protected function isV3Request(string $url): bool
return false;
}

public function withSerialHeader(?string $serial = null)
{
$platformCerts = $this->merchant->getPlatformCerts();
if (empty($platformCerts)) {
throw new InvalidConfigException('Missing platform certificate.');
}

$serial = $serial ?? array_key_first($platformCerts);
$this->withHeader('Wechatpay-Serial', $serial);
return $this;
}

/**
* @param array<int, mixed> $arguments
*
Expand Down Expand Up @@ -242,6 +254,17 @@ public static function createMockClient(MockHttpClient $mockHttpClient): HttpCli
Mockery::mock(PublicKey::class),
'mock-v3-key',
'mock-v2-key',
[
'PUB_KEY_ID_MOCK' => '-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlReZ1YnfAohRIfUqIeyP
aO0PlkMw1RLPdZbEZmldbGrIrOh/0XqSzNZ+mtB6H0eB7TSaoGFtdp/AWy3tb67m
1T62OrEhz6bnSKMcZkYVmODyxZvcwsCZ3zqCaFo7FrGmh1o9M0/Xfa5SOX4jVGni
3iM7r7YD/NiW2RCYDtjMoLTmVgrzv45Mzu2XpJqtNbUJIRRhVSnjsAZRC6spWH+b
QpYIkVd4qmYE0qdpIQBMYOV1w7v1pYn6Z5QdKG4keemADTn4QaZZHrryTcHNYVsZ
2OZ3aybrevSV3wDGnYGk2nt2xtkdfaNfFn4dGW+p4an5M4fRK+CnYpeTgI6POABk
pwIDAQAB
-----END PUBLIC KEY-----'
]
);

return Mockery::mock(static::class, [$mockMerchant, $mockHttpClient])
Expand Down
5 changes: 5 additions & 0 deletions src/Pay/Contracts/Merchant.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ public function getV2SecretKey(): ?string;
public function getCertificate(): PublicKey;

public function getPlatformCert(string $serial): ?PublicKey;

/**
* @return PublicKey[]
*/
public function getPlatformCerts(): array;
}
9 changes: 9 additions & 0 deletions src/Pay/Exceptions/EncryptionFailureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace EasyWeChat\Pay\Exceptions;

use EasyWeChat\Kernel\Exceptions\RuntimeException;

class EncryptionFailureException extends RuntimeException
{
}
5 changes: 5 additions & 0 deletions src/Pay/Merchant.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public function getPlatformCert(string $serial): ?PublicKey
return $this->platformCerts[$serial] ?? null;
}

public function getPlatformCerts(): array
{
return $this->platformCerts;
}

/**
* @param array<array-key, string|PublicKey> $platformCerts
* @return array<string, PublicKey>
Expand Down
Loading
Loading