Skip to content

Commit

Permalink
优化APIv3上的三个特殊接口验签逻辑,国内两个自动忽略验签,海外按spec仅验证RSA签名 (#95)
Browse files Browse the repository at this point in the history
* Two skipping pharses of the response's validation whose are located in the mainland.

One special Rsa::verify onto the downloading API which is located in the overseas.

Close #94

* bump to v1.4.5
  • Loading branch information
TheNorthMemory authored May 23, 2022
1 parent 0ddd126 commit 43b05b3
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 46 deletions.
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# 变更历史

## [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-pctencoded](https://github.com/guzzle/uri-template/issues/18)问题;
- 新增`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`返回,应用侧自动忽略验签;
- 新增`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

Expand Down
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,9 @@

## 项目状态

当前版本为 `1.4.4` 测试版本。项目版本遵循 [语义化版本号](https://semver.org/lang/zh-CN/)。如果你使用的版本 `<=v1.3.2`,升级前请参考 [升级指南](UPGRADING.md)

为了向广大开发者提供更好的使用体验,微信支付诚挚邀请您将**使用微信支付 API v3 SDK**中的感受反馈给我们。本问卷可能会占用您不超过2分钟的时间,感谢您的支持。

问卷系统使用的腾讯问卷,您可以点击[这里](https://wj.qq.com/s2/8779987/8dae/),或者扫描以下二维码参与调查。

[![PHP SDK Questionnaire](https://user-images.githubusercontent.com/1812516/126434257-834ef6ab-e66b-4aa2-9104-8e37d7a14b93.png)](https://wj.qq.com/s2/8779987/8dae/)
当前版本为 `1.4.5` 测试版本。
项目版本遵循 [语义化版本号](https://semver.org/lang/zh-CN/)
如果你使用的版本 `<=v1.3.2`,升级前请参考 [升级指南](UPGRADING.md)

## 环境要求

Expand Down
38 changes: 4 additions & 34 deletions README_APIv2.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,8 @@ $res = $instance
// 特殊接入点,仅对本次请求有效
'base_uri' => 'https://fraud.mch.weixin.qq.com/',
])
// 返回无sign字典,默认只能从异常通道获取返回值
->otherwise(static function($e) {
// 更多`$e`异常类型判断是必须的,这里仅列出可能的两种情况,请根据实际对接过程调整并增加
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
return Transformer::toArray((string)$e->getReason()->getBody());
}
if ($e instanceof \Psr\Http\Message\MessageInterface) {
return Transformer::toArray((string)$e->getBody());
}
return [];
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);
Expand Down Expand Up @@ -147,13 +139,6 @@ $res = $instance
->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);
```
Expand Down Expand Up @@ -186,13 +171,6 @@ $res = $instance
->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);
```
Expand All @@ -212,16 +190,8 @@ $res = $instance
// 通知SDK不接受沙箱环境重定向,仅对本次请求有效
'allow_redirects' => false,
])
// 返回无sign字典,只能从异常通道获取返回值
->otherwise(static function($e) {
// 更多`$e`异常类型判断是必须的,这里仅列出可能的两种情况,请根据实际对接过程调整并增加
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
return Transformer::toArray((string)$e->getReason()->getBody());
}
if ($e instanceof \Psr\Http\Message\MessageInterface) {
return Transformer::toArray((string)$e->getBody());
}
return [];
->then(static function($response) {
return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wechatpay/wechatpay",
"version": "1.4.4",
"version": "1.4.5",
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"type": "library",
"keywords": [
Expand Down
2 changes: 1 addition & 1 deletion src/ClientDecoratorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface ClientDecoratorInterface
/**
* @var string - This library version
*/
public const VERSION = '1.4.4';
public const VERSION = '1.4.5';

/**
* @var string - The HTTP transfer `xml` based protocol
Expand Down
38 changes: 37 additions & 1 deletion src/ClientJsonTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use function sprintf;
use function array_key_exists;
use function array_keys;
use function strcasecmp;
use function strncasecmp;

use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
Expand All @@ -30,6 +32,7 @@
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.
Expand Down Expand Up @@ -88,6 +91,13 @@ public static function signer(string $mchid, string $serial, $privateKey): calla
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(
Expand Down Expand Up @@ -117,10 +127,16 @@ protected static function assertSuccessfulResponse(array &$certs): callable
), $request, $response);
}

$isOverseas = 0 === strcasecmp($url, '/hk/v3/statements') && $response->hasHeader(WechatpayStatementSha1);

$verified = false;
try {
$verified = Crypto\Rsa::verify(
Formatter::response($timestamp, $nonce, static::body($response)),
Formatter::response(
$timestamp,
$nonce,
$isOverseas ? static::digestBody($response) : static::body($response)
),
$signature, $certs[$serial]
);
} catch (\Exception $exception) {}
Expand All @@ -135,6 +151,26 @@ protected static function assertSuccessfulResponse(array &$certs): callable
};
}

/**
* 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
*
* @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
*
Expand Down
45 changes: 45 additions & 0 deletions tests/OpenAPI/V3/MerchantService/Images/DownloadTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ public function testGet(array $config, string $slot, ResponseInterface $respondo
$this->mock->reset();
$this->mock->append($respondor);
$this->mock->append($respondor);
$this->mock->append($respondor);

$response = $endpoint->chain('v3/merchant-service/images/{media_slot_url}')->get([
'media_slot_url' => $slot,
]);
self::responseAssertion($response);

$response = $endpoint->chain('v3/merchant-service/images/{media_slot_url}')->get([
'handler' => $stack,
Expand Down Expand Up @@ -143,6 +149,13 @@ public function testGetAsync(array $config, string $slot, ResponseInterface $res
$this->mock->reset();
$this->mock->append($respondor);
$this->mock->append($respondor);
$this->mock->append($respondor);

$endpoint->chain('v3/merchant-service/images/{media_slot_url}')->getAsync([
'media_slot_url' => $slot,
])->then(static function (ResponseInterface $response) {
self::responseAssertion($response);
})->wait();

$endpoint->chain('v3/merchant-service/images/{media_slot_url}')->getAsync([
'handler' => $stack,
Expand Down Expand Up @@ -177,19 +190,37 @@ public function testUseStandardGuzzleHttpClient(array $config, string $slot, Res

$this->mock->reset();

$this->mock->append($respondor);
$response = $apiv3Client->request('GET', $relativeUrl);
self::responseAssertion($response);

$this->mock->append($respondor);
$response = $apiv3Client->request('GET', $relativeUrl, ['handler' => $stack]);
self::responseAssertion($response);

$this->mock->append($respondor);
$response = $apiv3Client->request('GET', $fullUri);
self::responseAssertion($response);

$this->mock->append($respondor);
$response = $apiv3Client->request('GET', $fullUri, ['handler' => $stack]);
self::responseAssertion($response);

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `get` method signature */
$response = $apiv3Client->get($relativeUrl);
self::responseAssertion($response);

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `get` method signature */
$response = $apiv3Client->get($relativeUrl, ['handler' => $stack]);
self::responseAssertion($response);

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `get` method signature */
$response = $apiv3Client->get($fullUri);
self::responseAssertion($response);

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `get` method signature */
$response = $apiv3Client->get($fullUri, ['handler' => $stack]);
Expand All @@ -199,17 +230,31 @@ public function testUseStandardGuzzleHttpClient(array $config, string $slot, Res
self::responseAssertion($response);
};

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `getAsync` method signature */
$response = $apiv3Client->getAsync($fullUri)->then($asyncAssertion)->wait();

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `getAsync` method signature */
$response = $apiv3Client->getAsync($fullUri, ['handler' => $stack])->then($asyncAssertion)->wait();

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `getAsync` method signature */
$response = $apiv3Client->getAsync($relativeUrl)->then($asyncAssertion)->wait();

$this->mock->append($respondor);
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `getAsync` method signature */
$response = $apiv3Client->getAsync($relativeUrl, ['handler' => $stack])->then($asyncAssertion)->wait();

$this->mock->append($respondor);
$response = $apiv3Client->requestAsync('GET', $relativeUrl)->then($asyncAssertion)->wait();

$this->mock->append($respondor);
$response = $apiv3Client->requestAsync('GET', $relativeUrl, ['handler' => $stack])->then($asyncAssertion)->wait();

$this->mock->append($respondor);
$response = $apiv3Client->requestAsync('GET', $fullUri)->then($asyncAssertion)->wait();

$this->mock->append($respondor);
$response = $apiv3Client->requestAsync('GET', $fullUri, ['handler' => $stack])->then($asyncAssertion)->wait();
}
Expand Down
Loading

0 comments on commit 43b05b3

Please sign in to comment.