Skip to content

Commit 43b05b3

Browse files
优化APIv3上的三个特殊接口验签逻辑,国内两个自动忽略验签,海外按spec仅验证RSA签名 (#95)
* 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
1 parent 0ddd126 commit 43b05b3

File tree

8 files changed

+235
-46
lines changed

8 files changed

+235
-46
lines changed

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
# 变更历史
22

3+
## [1.4.5](../../compare/v1.4.4...v1.4.5) - 2022-05-21
4+
5+
- 新增`APIv3`请求/响应特殊验签逻辑,国内两个下载接口自动忽略验签,海外商户账单下载仅验RSA签名,详见 [#94](https://github.com/wechatpay-apiv3/wechatpay-php/issues/94)
6+
- 新增`APIv3`[海外商户账单下载](https://pay.weixin.qq.com/wiki/doc/api/wxpay/ch/fusion_wallet_ch/QuickPay/chapter8_5.shtml)测试用例,示例说明如何验证流`SHA1`摘要;
7+
38
## [1.4.4](../../compare/v1.4.3...v1.4.4) - 2022-05-19
49

5-
- 新增`APIv3`[客诉图片下载](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter10_2_18.shtml)测试用例,示例说明如何避免[double-pctencoded](https://github.com/guzzle/uri-template/issues/18)问题;
10+
- 新增`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)问题;
611
- PHP内置函数`hash`方法在`PHP8`变更了返回值逻辑,代之为抛送`ValueError`异常,优化`MediaUtilTest`测试用例,以兼容`PHP7`;
7-
- 新增`APIv2`请求/响应白名单`URL`及调整验签逻辑,对于白名单内的请求,已知无`sign`返回,应用侧自动忽略验签;
12+
- 新增`APIv2`请求/响应白名单`URL`及调整验签逻辑,对于白名单内的请求,已知无`sign`返回,应用侧自动忽略验签,详见 [#92](https://github.com/wechatpay-apiv3/wechatpay-php/issues/92)
813

914
## [1.4.3](../../compare/v1.4.2...v1.4.3) - 2022-01-04
1015

README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,9 @@
2525

2626
## 项目状态
2727

28-
当前版本为 `1.4.4` 测试版本。项目版本遵循 [语义化版本号](https://semver.org/lang/zh-CN/)。如果你使用的版本 `<=v1.3.2`,升级前请参考 [升级指南](UPGRADING.md)
29-
30-
为了向广大开发者提供更好的使用体验,微信支付诚挚邀请您将**使用微信支付 API v3 SDK**中的感受反馈给我们。本问卷可能会占用您不超过2分钟的时间,感谢您的支持。
31-
32-
问卷系统使用的腾讯问卷,您可以点击[这里](https://wj.qq.com/s2/8779987/8dae/),或者扫描以下二维码参与调查。
33-
34-
[![PHP SDK Questionnaire](https://user-images.githubusercontent.com/1812516/126434257-834ef6ab-e66b-4aa2-9104-8e37d7a14b93.png)](https://wj.qq.com/s2/8779987/8dae/)
28+
当前版本为 `1.4.5` 测试版本。
29+
项目版本遵循 [语义化版本号](https://semver.org/lang/zh-CN/)
30+
如果你使用的版本 `<=v1.3.2`,升级前请参考 [升级指南](UPGRADING.md)
3531

3632
## 环境要求
3733

README_APIv2.md

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,8 @@ $res = $instance
103103
// 特殊接入点,仅对本次请求有效
104104
'base_uri' => 'https://fraud.mch.weixin.qq.com/',
105105
])
106-
// 返回无sign字典,默认只能从异常通道获取返回值
107-
->otherwise(static function($e) {
108-
// 更多`$e`异常类型判断是必须的,这里仅列出可能的两种情况,请根据实际对接过程调整并增加
109-
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
110-
return Transformer::toArray((string)$e->getReason()->getBody());
111-
}
112-
if ($e instanceof \Psr\Http\Message\MessageInterface) {
113-
return Transformer::toArray((string)$e->getBody());
114-
}
115-
return [];
106+
->then(static function($response) {
107+
return Transformer::toArray((string)$response->getBody());
116108
})
117109
->wait();
118110
print_r($res);
@@ -147,13 +139,6 @@ $res = $instance
147139
->then(static function($response) {
148140
return Transformer::toArray((string)$response->getBody());
149141
})
150-
->otherwise(static function($e) {
151-
// 更多`$e`异常类型判断是必须的,这里仅列出一种可能情况,请根据实际对接过程调整并增加
152-
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
153-
return Transformer::toArray((string)$e->getReason()->getBody());
154-
}
155-
return [];
156-
})
157142
->wait();
158143
print_r($res);
159144
```
@@ -186,13 +171,6 @@ $res = $instance
186171
->then(static function($response) {
187172
return Transformer::toArray((string)$response->getBody());
188173
})
189-
->otherwise(static function($e) {
190-
// 更多`$e`异常类型判断是必须的,这里仅列出一种可能情况,请根据实际对接过程调整并增加
191-
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
192-
return Transformer::toArray((string)$e->getReason()->getBody());
193-
}
194-
return [];
195-
})
196174
->wait();
197175
print_r($res);
198176
```
@@ -212,16 +190,8 @@ $res = $instance
212190
// 通知SDK不接受沙箱环境重定向,仅对本次请求有效
213191
'allow_redirects' => false,
214192
])
215-
// 返回无sign字典,只能从异常通道获取返回值
216-
->otherwise(static function($e) {
217-
// 更多`$e`异常类型判断是必须的,这里仅列出可能的两种情况,请根据实际对接过程调整并增加
218-
if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
219-
return Transformer::toArray((string)$e->getReason()->getBody());
220-
}
221-
if ($e instanceof \Psr\Http\Message\MessageInterface) {
222-
return Transformer::toArray((string)$e->getBody());
223-
}
224-
return [];
193+
->then(static function($response) {
194+
return Transformer::toArray((string)$response->getBody());
225195
})
226196
->wait();
227197
print_r($res);

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wechatpay/wechatpay",
3-
"version": "1.4.4",
3+
"version": "1.4.5",
44
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
55
"type": "library",
66
"keywords": [

src/ClientDecoratorInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface ClientDecoratorInterface
1414
/**
1515
* @var string - This library version
1616
*/
17-
public const VERSION = '1.4.4';
17+
public const VERSION = '1.4.5';
1818

1919
/**
2020
* @var string - The HTTP transfer `xml` based protocol

src/ClientJsonTrait.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use function sprintf;
1414
use function array_key_exists;
1515
use function array_keys;
16+
use function strcasecmp;
17+
use function strncasecmp;
1618

1719
use GuzzleHttp\Client;
1820
use GuzzleHttp\Middleware;
@@ -30,6 +32,7 @@
3032
const WechatpaySerial = 'Wechatpay-Serial';
3133
const WechatpaySignature = 'Wechatpay-Signature';
3234
const WechatpayTimestamp = 'Wechatpay-Timestamp';
35+
const WechatpayStatementSha1 = 'Wechatpay-Statement-Sha1';
3336

3437
/**
3538
* JSON based Client interface for sending HTTP requests.
@@ -88,6 +91,13 @@ public static function signer(string $mchid, string $serial, $privateKey): calla
8891
protected static function assertSuccessfulResponse(array &$certs): callable
8992
{
9093
return static function (ResponseInterface $response, RequestInterface $request) use(&$certs): ResponseInterface {
94+
if (
95+
0 === strcasecmp($url = $request->getUri()->getPath(), '/v3/billdownload/file')
96+
|| (0 === strncasecmp($url, '/v3/merchant-service/images/', 28) && 0 !== strcasecmp($url, '/v3/merchant-service/images/upload'))
97+
) {
98+
return $response;
99+
}
100+
91101
if (!($response->hasHeader(WechatpayNonce) && $response->hasHeader(WechatpaySerial)
92102
&& $response->hasHeader(WechatpaySignature) && $response->hasHeader(WechatpayTimestamp))) {
93103
throw new RequestException(sprintf(
@@ -117,10 +127,16 @@ protected static function assertSuccessfulResponse(array &$certs): callable
117127
), $request, $response);
118128
}
119129

130+
$isOverseas = 0 === strcasecmp($url, '/hk/v3/statements') && $response->hasHeader(WechatpayStatementSha1);
131+
120132
$verified = false;
121133
try {
122134
$verified = Crypto\Rsa::verify(
123-
Formatter::response($timestamp, $nonce, static::body($response)),
135+
Formatter::response(
136+
$timestamp,
137+
$nonce,
138+
$isOverseas ? static::digestBody($response) : static::body($response)
139+
),
124140
$signature, $certs[$serial]
125141
);
126142
} catch (\Exception $exception) {}
@@ -135,6 +151,26 @@ protected static function assertSuccessfulResponse(array &$certs): callable
135151
};
136152
}
137153

154+
/**
155+
* Downloading the reconciliation was required the client to format the `WechatpayStatementSha1` digest string as `JSON`.
156+
*
157+
* There was also sugguestion that to validate the response streaming's `SHA1` digest whether or nor equals to `WechatpayStatementSha1`.
158+
* Here may contains with or without `gzip` parameter. Both of them are validating the plain `CSV` stream.
159+
* Keep the same logic with the mainland's one(without `SHA1` validation).
160+
* If someone needs this feature built-in, contrubiting is welcome.
161+
*
162+
* @see https://pay.weixin.qq.com/wiki/doc/api/wxpay/ch/fusion_wallet_ch/QuickPay/chapter8_5.shtml
163+
* @see https://pay.weixin.qq.com/wiki/doc/api/wxpay/en/fusion_wallet/QuickPay/chapter8_5.shtml
164+
*
165+
* @param ResponseInterface $response - The response instance
166+
*
167+
* @return string - The JSON string
168+
*/
169+
protected static function digestBody(ResponseInterface $response): string
170+
{
171+
return sprintf('{"sha1":"%s"}', $response->getHeader(WechatpayStatementSha1)[0]);
172+
}
173+
138174
/**
139175
* APIv3's verifier middleware stack
140176
*

tests/OpenAPI/V3/MerchantService/Images/DownloadTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ public function testGet(array $config, string $slot, ResponseInterface $respondo
103103
$this->mock->reset();
104104
$this->mock->append($respondor);
105105
$this->mock->append($respondor);
106+
$this->mock->append($respondor);
107+
108+
$response = $endpoint->chain('v3/merchant-service/images/{media_slot_url}')->get([
109+
'media_slot_url' => $slot,
110+
]);
111+
self::responseAssertion($response);
106112

107113
$response = $endpoint->chain('v3/merchant-service/images/{media_slot_url}')->get([
108114
'handler' => $stack,
@@ -143,6 +149,13 @@ public function testGetAsync(array $config, string $slot, ResponseInterface $res
143149
$this->mock->reset();
144150
$this->mock->append($respondor);
145151
$this->mock->append($respondor);
152+
$this->mock->append($respondor);
153+
154+
$endpoint->chain('v3/merchant-service/images/{media_slot_url}')->getAsync([
155+
'media_slot_url' => $slot,
156+
])->then(static function (ResponseInterface $response) {
157+
self::responseAssertion($response);
158+
})->wait();
146159

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

178191
$this->mock->reset();
179192

193+
$this->mock->append($respondor);
194+
$response = $apiv3Client->request('GET', $relativeUrl);
195+
self::responseAssertion($response);
196+
180197
$this->mock->append($respondor);
181198
$response = $apiv3Client->request('GET', $relativeUrl, ['handler' => $stack]);
182199
self::responseAssertion($response);
183200

201+
$this->mock->append($respondor);
202+
$response = $apiv3Client->request('GET', $fullUri);
203+
self::responseAssertion($response);
204+
184205
$this->mock->append($respondor);
185206
$response = $apiv3Client->request('GET', $fullUri, ['handler' => $stack]);
186207
self::responseAssertion($response);
187208

209+
$this->mock->append($respondor);
210+
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `get` method signature */
211+
$response = $apiv3Client->get($relativeUrl);
212+
self::responseAssertion($response);
213+
188214
$this->mock->append($respondor);
189215
/** @phpstan-ignore-next-line because of \GuzzleHttp\ClientInterface no `get` method signature */
190216
$response = $apiv3Client->get($relativeUrl, ['handler' => $stack]);
191217
self::responseAssertion($response);
192218

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

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

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

249+
$this->mock->append($respondor);
250+
$response = $apiv3Client->requestAsync('GET', $relativeUrl)->then($asyncAssertion)->wait();
251+
210252
$this->mock->append($respondor);
211253
$response = $apiv3Client->requestAsync('GET', $relativeUrl, ['handler' => $stack])->then($asyncAssertion)->wait();
212254

255+
$this->mock->append($respondor);
256+
$response = $apiv3Client->requestAsync('GET', $fullUri)->then($asyncAssertion)->wait();
257+
213258
$this->mock->append($respondor);
214259
$response = $apiv3Client->requestAsync('GET', $fullUri, ['handler' => $stack])->then($asyncAssertion)->wait();
215260
}

0 commit comments

Comments
 (0)