评论

可定制的wechatpay-php,实现远程签名/验签服务方案参考

可定制化的 wechatpay-php 开发包,可用来解决商户私钥分发以及接口权限过大等问题。

之前在做开发时,意识到APIv3接口权限过大问题的一些建议。现在,你可以参考如下实现,来定制PHP版的APIv3开发包,使商户私钥以及接口请求安全管控可控。

定制

wechatpay/wechapay 开发包默认是使用的本地签名和验签方式来和官方接口交互,如对安全管控要求较高时,你可以通过实现signer及/或verifier中间件来定制签名服务或/及验签服务,来让你的系统把商户私钥集中存储,SDK从属的系统系统,仅需通过远程服务进行签名/验签。

以下示例用来演示如何替换SDK内置中间件,来实现远程请求签名结果验签,供大家参考实现。

服务定制

假设集中管理服务器接入点为内网http://192.168.169.170:8080/地址,并提供两个URI供签名及验签:

  • /wechatpay-merchant-request-signature 为请求签名
  • /wechatpay-response-merchant-validation 为响应验签

SDK中间件定制

use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$client = new Client(['base_uri' => 'http://192.168.169.170:8080/']);

// 请求参数签名,返回字符串形如`\WeChatPay\Formatter::authorization`返回的字符串
$remoteSigner = function (RequestInterface $request) use ($client, $merchantId): string {
    return (string)$client->post('/wechatpay-merchant-request-signature', ['json' => [
        'mchid' => $merchantId,
        'verb'  => $request->getMethod(),
        'uri'   => $request->getRequestTarget(),
        'body'  => (string)$request->getBody(),
    ]])->getBody();
};

// 返回结果验签,返回可以是4xx,5xx,与远程验签应用约定返回字符串'OK'为验签通过
$remoteVerifier = function (ResponseInterface $response) use ($client, $merchantId): string {
    [$nonce]     = $response->getHeader('Wechatpay-Nonce');
    [$serial]    = $response->getHeader('Wechatpay-Serial');
    [$signature] = $response->getHeader('Wechatpay-Signature');
    [$timestamp] = $response->getHeader('Wechatpay-Timestamp');
    return (string)$client->post('/wechatpay-response-merchant-validation', ['json' => [
        'mchid'     => $merchantId,
        'nonce'     => $nonce,
        'serial'    => $serial,
        'signature' => $signature,
        'timestamp' => $timestamp,
        'body'      => (string)$response->getBody(),
    ]])->getBody();
};

正常初始化

仅需保证商户号为正确商户号即可,其他必填参数,可以任意值,例如:

use WeChatPay\Builder;

// 商户号,假定为`1000100`
$merchantId = '1000100';
// 工厂方法构造一个实例
$instance = Builder::factory([
    'mchid'      => $merchantId,
    'serial'     => 'nop',
    'privateKey' => 'any',
    'certs'      => [ 'any' => null],
]);

注册及替换SDK默认中间件

$stack = $instance->getDriver()->select()->getConfig('handler');
// 卸载SDK内置签名中间件
$stack->remove('signer');
// 注册内网远程请求签名中间件
$stack->before('prepare_body', Middleware::mapRequest(
    static function (RequestInterface $request) use ($remoteSigner): RequestInterface {
        return $request->withHeader('Authorization', $remoteSigner($request));
    }
), 'signer');
// 卸载SDK内置验签中间件
$stack->remove('verifier');
// 注册内网远程请求验签中间件
$stack->before('http_errors', static function (callable $handler) use ($remoteVerifier): callable {
    return static function (RequestInterface $request, array $options = []) use ($remoteVerifier, $handler) {
        return $handler($request, $options)->then(
            static function(ResponseInterface $response) use ($remoteVerifier, $request): ResponseInterface {
                $verified = '';
                try {
                    $verified = $remoteVerifier($response);
                } catch (\Throwable $exception) {}
                if ($verified === 'OK') { //远程验签约定,返回字符串`OK`作为验签通过
                    throw new RequestException('签名验签失败', $request, $response, $exception ?? null);
                }
                return $response;
            }
        );
    };
}, 'verifier');

正常 Chainable/PromiseA+ 使用SDK

异步模式请求平台证书接口

$instance
->V3->Certificates
->getAsync()
->then(static function($res) { return $res->getBody(); })
->wait();

退款

$res = $instance
->chain('v3/refund/domestic/refunds')
->postAsync([
    'json' => [
        'transaction_id' => '1217752501201407033233368018',
        'out_refund_no'  => '1217752501201407033233368018',
        'amount'         => [
            'refund'   => 888,
            'total'    => 888,
            'currency' => 'CNY',
        ],
    ],
])
->then(static function($response) {
    // 正常逻辑回调处理
    // do something
})
->otherwise(static function($e) {
    // 异常错误处理
    // do something
})
->wait();

备注

远程请求签名,可以按需加入ACL,推荐对请求的 VERB+URI 做权限控制,即可解开商户私钥分发以及接口权限过大等问题。

最后一次编辑于  2021-12-26  
点赞 1
收藏
评论

1 个评论

  • 平静
    平静
    发表于移动端
    2021-08-10
    ,,
    2021-08-10
    赞同
    回复
登录 后发表内容