Skip to content

Commit

Permalink
feat: 添加平台公钥加密功能及补充文档 (#2872)
Browse files Browse the repository at this point in the history
* feat: 添加平台公钥加密功能及补充文档

* fix: 单词拼写错误

* fix: 单词拼写错误,调整文案表述

* feat: 增加加密时自动携带Wechatpay-Serial处理逻辑

* Revert "feat: 增加加密时自动携带Wechatpay-Serial处理逻辑"

This reverts commit 6a79ad1.

* feat: 请求头增加withSerialHeader

* Update Utils.php

* Update UtilsTest.php

* Update utils.md

* Update Utils.php

* Update UtilsTest.php

---------

Co-authored-by: 安正超 <[email protected]>
  • Loading branch information
lyt8384 and overtrue authored Jan 7, 2025
1 parent d278fb1 commit 5152681
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 47 deletions.
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

0 comments on commit 5152681

Please sign in to comment.