wechatpay-apiv3 / wechatpay-php

微信支付 APIv3 的官方 PHP Library,同时也支持 APIv2
Apache License 2.0
475 stars 98 forks source link

求帮忙看一下代码对不对,移动端调用一直提示签名验证失败 #119

Closed wangya123456789 closed 1 year ago

wangya123456789 commented 1 year ago

运行环境

- OS:Nginx 1.22.1
- PHP:7.4.33
- wechatpay-php:1.4.8

描述你的问题现象

App是使用Flutter开发的,使用fluwx微信支付插件,移动端一直提示支付验证签名失败,我还配置了拉起小程序的功能,没遇到问题,就是支付一直报错

Postman测试结果 image

这个签名验证工具,不管我输入什么明文他都提示验证通过 image


<?php

namespace app\controller;

use app\BaseController;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;
use WeChatPay\Builder;
use WeChatPay\Formatter;

class Index extends BaseController
{
    public function index()
    {

        // 商户号

        $merchantId = '16443******';

        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = 'file://' . './WechatKey/apiclient_key.pem';

        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

        // 「商户API证书」的「证书序列号」
        $merchantCertificateSerial = '624BE588C745175E5557DA9D29AD*******';

        // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
        $platformCertificateFilePath = 'file://' . './WechatKey/cert.pem';
        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);

        // 构造一个 APIv3 客户端实例
        $instance = Builder::factory([
            'mchid'      => $merchantId,
            'serial'     => $merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $platformPublicKeyInstance,
            ],
        ]);

        // 发送请求
        $resp = $instance->chain('v3/certificates')->get(
            ['debug' => false] // 调试模式,https://docs.guzzlephp.org/en/stable/request-options.html#debug
        );
        //echo $resp->getBody(), PHP_EOL;

        //第二步,生成prepay_id,以及sign签名
        try {
            $resp = $instance
                ->chain('v3/pay/transactions/app')
                ->post(['json' => [
                    'mchid'        => '16443******',
                    'out_trade_no' => $this->FunctionName(),
                    'appid'        => 'wx1d3659d570********',
                    'description'  => '友情赞助',
                    'notify_url'   => 'https://www.weixin.qq.com/wxpay/pay.php', //用来接收,用户是否支付成功的链接
                    'amount'       => [
                        'total'    => 1, //订单金额
                        'currency' => 'CNY'
                    ],
                ]]);

            //echo $resp->getStatusCode(), PHP_EOL;
            //echo $resp->getBody(), PHP_EOL;
            $obj = json_decode($resp->getBody());
            $sign = json_decode($this->sign($obj->prepay_id));

            $ret = ["prepay_id" => $obj->prepay_id, "sign" => $sign->paySign];
            return json_encode($ret); //返回sign签名和prepay_id
        } catch (\Exception $e) {
            // 进行错误处理
            echo $e->getMessage(), PHP_EOL;
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
                echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
            }
            echo $e->getTraceAsString(), PHP_EOL;
        }
    }

    //随机生成一个订单号
    public function FunctionName()
    {
        // 定义字符集
        $chars = '0123456789';

        // 生成随机字符串
        $randomString = '';
        for ($i = 0; $i < 18; $i++) {
            $index = rand(0, strlen($chars) - 1);
            $randomString .= $chars[$index];
        }

        return $randomString . time(); // 输出随机字符串
    }

    //生成sign签名
    public function sign(string $prepay_id)
    {
        $merchantPrivateKeyFilePath = 'file://' . './WechatKey/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);

        $params = [
            'appId'     => 'wx1d3659d5******',
            'timeStamp' => (string)Formatter::timestamp(),
            'nonceStr'  => Formatter::nonce(),
            'package'   => 'prepay_id=' . $prepay_id,
        ];
        $params += ['paySign' => Rsa::sign(
            Formatter::joinedByLineFeed(...array_values($params)),
            $merchantPrivateKeyInstance
        ), 'signType' => 'RSA'];

        return json_encode($params);
    }
}
TheNorthMemory commented 1 year ago
$resp = $instance->chain('v3/certificates')

这个请求仅是README示例,在实际项目内,几乎用不到,建议拿掉...

$ret = ["prepay_id" => $obj->prepay_id, "sign" => $sign->paySign];
return json_encode($ret); //返回sign签名和prepay_id

这里的return 会把 sign 做二次json序列话,你前端Flutter拿到要做一次JSON.parse成为对象再送给 wx.requestPayment

前端看不到你是怎么处理的,大致推断是你前端把 sign 当成字符串给 wx.requestPayment 的缘由。

TheNorthMemory commented 1 year ago

另外注意看一下 #116 , 你前端用的API是拿锅,你要查拿锅儿开放平台的官方文档,有些地方入参要求是 timestamp 有些地方要求是 timeStamp, 拿锅儿 ~S~ 字符大小写得按MP端的文档来,该大写大写,该小写小写...

wangya123456789 commented 1 year ago

另外注意看一下 #116 , 你前端用的API是拿锅,你要查拿锅儿开放平台的官方文档,有些地方入参要求是 timestamp 有些地方要求是 timeStamp, 拿锅儿 ~S~ 字符大小写得按MP端的文档来,该大写大写,该小写小写...

好的,我研究一下

wangya123456789 commented 1 year ago
$resp = $instance->chain('v3/certificates')

这个请求仅是README示例,在实际项目内,几乎用不到,建议拿掉...

$ret = ["prepay_id" => $obj->prepay_id, "sign" => $sign->paySign];
return json_encode($ret); //返回sign签名和prepay_id

这里的return 会把 sign 做二次json序列话,你前端Flutter拿到要做一次JSON.parse成为对象再送给 wx.requestPayment

前端看不到你是怎么处理的,大致推断是你前端把 sign 当成字符串给 wx.requestPayment 的缘由。

还是报错 支付验证失败,下面是我的Flutter代码

Response response = await Dio().post('http://pay.yyybabc.com/index.php', data: {'money': money.value});
Map<String, dynamic> data =await json.decode(response.data);
await fluwx.pay(
    which: Payment(
       appId: "wx1d3659d57006b7d0",
       partnerId: '1644348856',
       prepayId: data['prepay_id'].toString(),
       packageValue: 'Sign=WXPay',
       nonceStr: noncestr().toString(),
      timestamp: DateTime.now().second ,
      sign: data['sign'].toString(),
));
print("支付结果:${_result}");

下面是请求发起到结束时的运行打印内容

D/MicroMsg.SDK.WXMsgImplComm(20143): check signature:308202eb30820254a00302010202044d36f7a4300d06092a864886f70d01010505003081b9310b300906035504061302383631123010060355040813094775616e67646f6e673111300f060355040713085368656e7a68656e31353033060355040a132c54656e63656e7420546563686e6f6c6f6779285368656e7a68656e2920436f6d70616e79204c696d69746564313a3038060355040b133154656e63656e74204775616e677a686f7520526573656172636820616e6420446576656c6f706d656e742043656e7465723110300e0603550403130754656e63656e74301e170d3131303131393134333933325a170d3431303131313134333933325a3081b9310b300906035504061302383631123010060355040813094775616e67646f6e673111300f060355040713085368656e7a68656e31353033060355040a132c54656e63656e7420546563686e6f6c6f6779285368656e7a68656e2920436f6d70616e79204c696d69746564313a3038060355040b133154656e63656e74204775616e677a686f7520526573656172636820616e6420446576656c6f706d656e742043656e7465723110300e0603550403130754656e63656e7430819f300d06092a864886f70d010101050003818d0030818902818100c05f34b231b083fb1323670bfbe7bdab40c0c0a6efc87ef2072a1ff0d60cc67c8edb0d0847f210bea6cbfaa241be70c86daf56be08b723c859e52428a064555d80db448cdcacc1aea2501eba06f8bad12a4fa49d85cacd7abeb68945a5cb5e061629b52e3254c373550ee4e40cb7c8ae6f7a8151ccd8df582d446f39ae0c5e930203010001300d06092a864886f70d0101050500038181009c8d9d7f2f908c42081b4c764c377109a8b2c70582422125ce545842d5f520aea69550b6bd8bfd94e987b75a3077eb04ad341f481aac266e89d3864456e69fba13df018acdc168b9a19dfd7ad9d9cc6f6ace57c746515f71234df3a053e33ba93ece5cd0fc15f3e389a3f365588a9fcb439e069d3629cd7732a13fff7b891499
D/MicroMsg.SDK.WXMsgImplComm(20143): pass
I/MicroMsg.SDK.WXApiImplV10(20143): sendReq, req type = 5
I/MicroMsg.SDK.WXApiImplV10(20143): getTokenFromWX token is wx1d3659d57006b7d0--557596528-25708289922379-193
I/MicroMsg.SDK.MMessageAct(20143): send, targetPkgName = com.tencent.mm, targetClassName = com.tencent.mm.plugin.base.stub.WXPayEntryActivity, launchMode = 2
I/MicroMsg.SDK.MMessageAct(20143): sendUsingPendingIntent
D/MicroMsg.SDK.MMessageAct(20143): send mm message, intent=Intent { flg=0x18000000 cmp=com.tencent.mm/.plugin.base.stub.WXPayEntryActivity (has extras) }
I/flutter (20143): 支付结果:无
I/MicroMsg.SDK.MMessageAct(20143): sendUsingPendingIntent onSendFinished resultCode: 0, resultData: null
W/SQLiteLog(20143): (28) double-quoted string literal: "
W/SQLiteLog(20143): (28) double-quoted string literal: "30E48AC1072C76BE12EF5DEBF5788F56"
D/DecorView[](20143): onWindowFocusChanged hasWindowFocus false
D/BrandVideoCacheManager(20143): onReceivedNewBrandCache start:2
D/BrandVideoCacheManager(20143): setting num:2
D/BrandVideoCacheManager(20143): save video cache:[]
D/BrandVideoCacheManager(20143): try delete: edf9a24f85e4da94183ec64b127e06b1 ,result false
E/flutter (21842): [ERROR:flutter/lib/ui/text/fontmgr_default_android.cc(426)] value Xiaomi
E/flutter (21842): [ERROR:flutter/lib/ui/text/fontmgr_default_android.cc(428)] fontXmlName /data/user/0/com.tencent.mm/code_cache/com.tencent.mm:appbrand0/flutter_custom_fonts2.xml
E/flutter (21842): [ERROR:flutter/lib/ui/text/fontmgr_default_android.cc(541)] use default font mgr
I/flutter (21842): [INFO:message_loop_android.cc(22)] prepare main_looper: 1
I/flutter (21842): [INFO:message_loop.cc(104)] start create platform thread message loop
I/flutter (21842): [INFO:message_loop_android.cc(49)] AcquireLooperForMainThread success
I/flutter (21842): [INFO:message_loop.cc(106)] create platform thread message loop end
I/flutter (21842): [INFO:message_loop.cc(108)] platform thread loop set to thread local message loop
D/BrandVideoCacheManager(20143): onReceivedNewBrandCache start:2
D/BrandVideoCacheManager(20143): setting num:2
D/BrandVideoCacheManager(20143): save video cache:[]
D/BrandVideoCacheManager(20143): try delete: edf9a24f85e4da94183ec64b127e06b1 ,result false
I/TeaLog  (20143): s worked:true 60000
D/TrafficStats(20143): tagSocket(86) with statsTag=0xffffffff, statsUid=-1
I/TeaLog  (20143): s 1 1
I/TeaLog  (20143): s worked:true 60000
D/TrafficStats(20143): tagSocket(85) with statsTag=0xffffffff, statsUid=-1
I/TeaLog  (20143): s worked:true 60000
I/com.yyybabc(20143): This is non sticky GC, maxfree is 8388608 minfree is 2097152
I/TeaLog  (20143): s worked:true 60000
TheNorthMemory commented 1 year ago

iOS sample@ https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_4.shtml

PayReq *request = [[[PayReq alloc] init] autorelease];
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==";
[WXApi sendReq:request];

backend:

    public function sign(string $prepay_id, string $app_id = 'wx1d3659d57006b7d0', string $mch_id = '1644348856')
    {
        $merchantPrivateKeyFilePath = 'file://' . './WechatKey/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);

        $params = [
            'appid'     => $app_id,
            'timestamp' => (string)Formatter::timestamp(),
            'noncestr'  => Formatter::nonce(),
            'prepayid'  => $prepay_id,
        ];
        $params += ['sign' => Rsa::sign(
            Formatter::joinedByLineFeed(...array_values($params)),
            $merchantPrivateKeyInstance
        ), 'partnerid' => $mch_id, 'package' => 'Sign=WXPay', ];

        return $params;
    }

front:

await fluwx.pay(
    which: Payment(
       appId: data['appid'].toString(),
       partnerId: data['partnerid'].toString(),
       prepayId: data['prepayid'].toString(),
       packageValue: data['package'].toString(),
       nonceStr: data['noncestr'].toString(),
       timeStamp: data['timestamp'].toString(),
       sign: data['sign'].toString(),
));
wangya123456789 commented 1 year ago

什么意思,没看明白,这不就是我的代码吗?

wangya123456789 commented 1 year ago

成功了, 'package' => 'prepay_id=' . $prepay_id,不能有prepay_id=,唉,你们文档也不说一下 //生成sign签名 public function sign(string $prepay_id) { $merchantPrivateKeyFilePath = 'file://' . './WechatKey/apiclient_key.pem'; $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);

    $params = [
        'appId'     => 'wx1d3659d5******',
        'timeStamp' => (string)Formatter::timestamp(),
        'nonceStr'  => Formatter::nonce(),
        'package'   => **'prepay_id=' .** $prepay_id,
    ];
    $params += ['paySign' => Rsa::sign(
        Formatter::joinedByLineFeed(...array_values($params)),
        $merchantPrivateKeyInstance
    ), 'signType' => 'RSA'];

    return json_encode($params);
}
TheNorthMemory commented 1 year ago

app用于唤醒微信收银台的参数,建议均以服务端返回的为准,你调整后的代码能运行,是运气;建议你仔细看一下我给你贴的 backend front 关键代码。