运行环境
wechatpay-php: ^1.4.4
需求
引申自关于v2接口中间件强行要求验证sign字段问题 #92, 当前已知在 APIv3
的三个接口上,有三种“特殊”逻辑,当前版本SDK并没有特殊处理,如下:
1. 国内商户客诉图片下载
URL: /v3/merchant-service/images/{slot}
接口本身返回的流stream,无验签逻辑;
2. 国内商户账单下载
URL: /v3/billdownload/file
接口本身返回的流stream,验签逻辑依赖前置请求账单的SHA1
摘要,而前置接口的返回的SHA1
值,已按标准RSA公钥逻辑验签;
3. 海外商户账单下载
URL: /hk/v3/statements
请求的返回值,签名含两种逻辑,标准RSA公钥验签逻辑<sup>注1</sup>及流stream SHA1
摘要逻辑;
注1: 验签的是 SHA1
摘要的JSON
表达式,而非流,与标准RSA公钥验签验证返回BODY
不同;
分析
-
第1种情形,接口设计的比较另类另说(见 增加了两条APIv3的测试用例,示例说明图片下载及上传的特殊用法 #85 ),无验签逻辑需要客户端自动忽略;
-
第2种是两步
请求
分开验签,先RSA
后SHA1
,验签逻辑含上下文context
语境,后置的客户端本地验签逻辑可循 以函数链的形式流式下载交易帐单 示例,在->then
链上处理,当前请求返回值无明确签名,客户端需要忽略验签; -
第3种情形可以看作是第2种的特例,一步请求,接口即回吐了
RSA
签名又包含SHA1
摘要,客户端设计可以仿第2种逻辑,仅对RSA签名部分进行验签,保证当前请求的返回值,是来自平台RSA
私钥数据签名<sup>注2</sup>,SHA1
摘要逻辑交由客户端本地在->then
链上自行比对摘要值;
注2: 按照规范,非对称加密的公钥是公开的,可任意发行的,而平台RSA私钥
是私密数据,一次请求即含签名又含摘要,虽然任何人(持有效公钥)都可以验签,但RSA签名值
本身是不可伪造,是安全的。
改进
按照上述分析,对前两种接口设计,当次请求/响应需要忽略RSA验签
;对第三种逻辑,需要构造一个本地摘要值JSON
串,做一次RSA公钥验签
,验签若失败,则按当前处理逻辑抛\GuzzleHttp\Exception\RequestException
异常;RSA验签
成功则(忽略SHA1
值比对逻辑)放行。
实现
对\WeChatPay\ClientJsonTrait::assertSuccessfulResponse
做逻辑补丁加强,代码类似如下:
diff --git a/src/ClientJsonTrait.php b/src/ClientJsonTrait.php
index 7957f21..af8d870 100644
--- a/src/ClientJsonTrait.php
+++ b/src/ClientJsonTrait.php
@@ -13,6 +13,9 @@ use function count;
use function sprintf;
use function array_key_exists;
use function array_keys;
+use function in_array;
+use function strncasecmp;
+use function stripos;
use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
@@ -88,6 +91,16 @@ trait ClientJsonTrait
protected static function assertSuccessfulResponse(array &$certs): callable
{
return static function (ResponseInterface $response, RequestInterface $request) use(&$certs): ResponseInterface {
+ if (
+ in_array($url = $request->getRequestTarget(), [/*mainland*/ '/v3/billdownload/file', /*overseas*/ '/hk/v3/statements'])
+ || /*the complaint image*/ (0 === strncasecmp($url, '/v3/merchant-service/images/', 28) && false === stripos($url, 'upload', 28))
+ ) {
+ // Notes: For overseas developers, the response's validation was different with the mainland's architecture,
+ // and it was related with both of the stream's `SHA1` and the `Wechatpay-Statement-Sha1` header value.
+ // Skip and postpone here for contributing.
+ return $response;
+ }
+
if (!($response->hasHeader(WechatpayNonce) && $response->hasHeader(WechatpaySerial)
&& $response->hasHeader(WechatpaySignature) && $response->hasHeader(WechatpayTimestamp))) {
无需思考,直接点赞!
1.4.5 - 2022-05-21
APIv3
请求/响应特殊验签逻辑,国内两个下载接口自动忽略验签,海外商户账单下载仅验RSA签名,详见 #94;APIv3
海外商户账单下载测试用例,示例说明如何验证流SHA1
摘要;