wechatpay-apiv3 / wechatpay-apache-httpclient

微信支付 APIv3 Apache HttpClient装饰器(decorator)
https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/index.shtml
Apache License 2.0
660 stars 249 forks source link

回调验签失败 #152

Closed AFatMan closed 2 years ago

AFatMan commented 2 years ago

异常信息 com.wechat.pay.contrib.apache.httpclient.exception.ValidationException: 验签失败:serial=[4FE088A8E7AC57DBA7A29F37BAA76ADA8F9EAD05] message=[1655705237 81xkrdXEvgG5vM2ROhHJybbEwq84n3MG {"summary":"支付成功","resource":{"ciphertext":"B7tyg5HKxbeTURxDdPs3i50gyDBpYQvQUHi9v1dyrXWIyzifKcSv9Wmc76DUsqUs4PSdplOxtdf83dG7+By2wt1Odmj9VmG9KmFQi9Lwxl+kH4MV87mm55lJzhplN0zpCoEQzyMWZ+b6pNCQCc8/lxE7H40Y8n+LUIPdFFf2zcPA789PckDcE1XrUudu6JnhbAw3UywCv/P/Z6r056HH4Xpjq1ny+F9K4FMBwHPiphd706EHK5NUJz3FKwYQWj+V+nqKuWDsPRbLFZTUliFsjTJ9yxAjZbEP1zIfTLlfQ3kLgQtblr6VTu/hZCn5LKecsHvyKvCuDN2l31ohiBf/uZqDoesy4NxYp2mWz5jmsqxxIMkkl+7fJYVdLdIyOJPll6QS9NVAXZvjAC5BlmkaWf3cnhfDqWHsxvba9+pOeBJ5jJohOC40cLAPtmGEi8H+kij6B2ie3mhHKibd3ctxy36y2yAH/cmSYbI1YH1IspoNiQofu5mUf1VcFGfYWTmi9ruE/N5QOyctK354nquhBbEBwwj47DKvq2Gu4JHMt/QPFRReZ3bb7nch+NdqNtdIgPorY7123mzTpEIHNDQe","originalType":"transaction","associatedData":"transaction","nonce":"JiRoi3NoMIPo","algorithm":"AEAD_AES_256_GCM"},"eventType":"TRANSACTION.SUCCESS","createTime":"2022-06-20T14:07:17+08:00","id":"a61e735c-0006-562c-afa4-ebb2819e8124","resourceType":"encrypt-resource"} ] sign=[Hsdtq936PC1zZ6CI2cunCO0ruqcgg1ox7ujwSpbkaPIqPoQN0m2kovae8K2Pp7rhgpid70XUR+nuilslOuNBZmPrugSJJwoqe/wuHv3oZ/YUYjv7SBCKoFpm5qFVPB5/HXOvOh9QWpkupram+2uv/1Z4M/v+nwXKBbK7QwKQL7lFjUJ9qMHKVlbaUpyZpifJrr1sdLXuLMc7tcfv36x6ofuio5Kfwl8qr97da0BIGjj238P4JvgBTH/U3Ff1hRYlHX80xTsQgpxFQ3eglEqGNeqkQtU32YAgfDTGBeoIOGi2D7gpdksgUGd2WZ0bE0pBIiH6dyw4C29mbtdYCNsV8w==]

lianup commented 2 years ago

可以先对比一下传入NotificationHandler的信息serialNumber、timestamp、nonce、body是否跟原始的HTTP请求参数完全一致,不一致会导致验签失败。

AFatMan commented 2 years ago

可以先对比一下传入NotificationHandler的信息serialNumber、timestamp、nonce、body是否跟原始的HTTP请求参数完全一致,不一致会导致验签失败。

从请求参数上获取的,是一致的

lianup commented 2 years ago

方便的话麻烦贴一下设置参数的代码,以及检查下body参数有没有编码问题,例如=号转成\u003d的问题。

AFatMan commented 2 years ago
public String nativeNotify(HttpServletRequest request, HttpServletResponse response) throws ValidationException, ParseException {
        //获取body
        StringBuilder sb = new StringBuilder();

        try (ServletInputStream inputStream = request.getInputStream();
             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        ) {
            String line;

            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }

        } catch (IOException e) {
            log.error("读取数据流异常:{}", e.toString());
        }

        log.info("body:" + sb);

        String body = sb.toString();

        //随机串
        String nonceStr = request.getHeader("Wechatpay-Nonce");

        //微信传递过来的签名
        String signature = request.getHeader("Wechatpay-Signature");

        //证书序列号(微信平台)
        String serialNo = request.getHeader("Wechatpay-Serial");

        //时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");

        log.info("通知回调请求参数: body: {} ,nonceStr: {} ,signature: {} ,serialNo: {} ,timestamp: {}", body, nonceStr,
                signature,
                serialNo,
                timestamp);
        // 签名的验证及解密
        // 构建request,传入必要参数
        NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serialNo)
                .withNonce(nonceStr)
                .withTimestamp(timestamp)
                .withSignature(signature)
                .withBody(body)
                .build();
        NotificationHandler handler = new NotificationHandler(wxPayVerifier, "apiv3密钥".getBytes(StandardCharsets.UTF_8));
        // 验签和解析请求体
        Notification notification = handler.parse(notificationRequest);
        // 从notification中获取解密报文
        System.out.println(notification.getDecryptData());
        log.info("支付通知的解密数据: {}", notification.getDecryptData());
        log.info("通知验签成功");
        response.setStatus(200);

        return new JSONObject() {{
            set("code", "SUCCESS");
            set("message", "成功");
        }}.toString();
    }

    /* 
    配置类
    */
    @Bean
    @SneakyThrows
    public Verifier wxPayVerifier(WxPayProperties wxPayProperties) {
        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 私钥签名对象(签名)
        PrivateKeySigner privateKeySigner = new PrivateKeySigner("商户证书序列号", wxPayPrivateKey(wxPayProperties));
        // 身份认证对象(验签)
        WechatPay2Credentials credentials = new WechatPay2Credentials("商户号",
                privateKeySigner);
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant("商户号", credentials, "apiv3密钥".getBytes(StandardCharsets.UTF_8));
        // 使用定时更新的签名验证器,不需要传入证书
        return certificatesManager.getVerifier("商户号");
    }

    // 私钥
    @Bean
    public PrivateKey wxPayPrivateKey(WxPayProperties wxPayProperties) {
        try {
            InputStream inputStream = new ClassPathResource("私钥文件.pem路径").getInputStream();
            return PemUtil.loadPrivateKey(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("私钥文件不存在", e);
        }
    }
AFatMan commented 2 years ago

还需要额外补充其他代码么?

lianup commented 2 years ago

我测试了你提供的这个id为a61e735c-0006-562c-afa4-ebb2819e8124的回调通知,使用原始的网络请求参数是可以通过的。 看代码header的获取应该没问题,建议调试对比下网络请求的header参数和body参数和真正获取的是否一致。

xy-peng commented 2 years ago
try (ServletInputStream inputStream = request.getInputStream();
     BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
    String line;

    while ((line = reader.readLine()) != null) {
        sb.append(line);
    }
} 

@AFatMan 不要用 BufferedReader,readline()读入时,返回的 line 中没有包含行尾的换行,导致 body 和原始数据不一致了。

类似于下面这样子。

 ByteArrayOutputStream result = new ByteArrayOutputStream();
 byte[] buffer = new byte[1024];
 for (int length; (length = inputStream.read(buffer)) != -1; ) {
     result.write(buffer, 0, length);
 }

更多更详细,可以参考How do I read / convert an InputStream into a String in Java?

AFatMan commented 2 years ago

首先,感谢大佬这么晚仍在为我解答疑惑 @xy-peng 然后使用您推荐的种方法,并且我把我代码里之前某些地方用的new替换为spring管理的bean后,验签成功了 感谢!!!

AFatMan commented 2 years ago

@lianup 感谢您的解答,发现是我代码的问题,解决后验签成功了

youyou-pm10 commented 9 months ago

哎,我也遇到一样的问题了。。。换行符有关的都尝试了一遍,还是不行


public Transaction validSign(HttpServletRequest request) throws ValidationException{
        String wechatPaySerial = request.getHeader("Wechatpay-Serial");
        String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
        String wechatSignature = request.getHeader("Wechatpay-Timestamp");
        String wechatTimestamp = request.getHeader("Wechatpay-Signature");
        String requestBody = getRequestBody(request);
        // 构造 RequestParam
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(wechatPaySerial)
                .nonce(wechatpayNonce)
                .signature(wechatSignature)
                .timestamp(wechatTimestamp)
                .body(requestBody)
                .build();

        // 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
        // 没有的话,则构造一个
        NotificationConfig config = new RSAAutoCertificateConfig.Builder()
                .merchantId(merchantId)
                .privateKeyFromPath(privateKeyPath)
                .merchantSerialNumber(merchantSerialNumber)
                .apiV3Key(apiV3Key)
                .build();
        // 初始化 NotificationParser
        NotificationParser parser = new NotificationParser(config);
        return parser.parse(requestParam, Transaction.class);
    }
youyou-pm10 commented 9 months ago

 private String getRequestBody(HttpServletRequest request) {
        String body;
        try (ServletInputStream inputStream = request.getInputStream()
        ) {
            ByteArrayOutputStream result = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            for (int length; (length = inputStream.read(buffer)) != -1; ) {
                result.write(buffer, 0, length);
            }
            body = result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return body;
    }
youyou-pm10 commented 9 months ago

2023-10-11T23:14:53.101+08:00 INFO 248827 --- [nio-8081-exec-2] c.example.demo.controller.BuyController : notice pay failed: sign verification failed! Processing WechatPay notification,signature verification failed,signType[WECHATPAY2-SHA256-RSA2048] serial[77A42067DABBAF4B76EA6575AFD598CF712D604B] message[lXPX2+KDHxZY23S02fBACOCHvhr+Mbu6JkcA/j3cjWNYSOYH8cH+FlaeKExLWxOS4WPf+PshtED3yaHn/mYMBcQ7CAYYtGe7zWQVIQXELqrTL28aIkOqKxFagRbFVgzhmjJLCCIzWQNdasl6CTsEnCwFGA9W76OCeBK10N5Hl+iZqUP81qBlx09J+WBpRtAsNIJorbqXCj+ZuIDjvqlfF16woiJG+9wgbJBQ2tV3/+FDA232XBj381dV8/npnY2ZeD0Rdfj0M/RGMjIiRbAfbctjHe7jQYVKuRxn8iejiI6DBRZRgFm3NhW9VyT3CNsc3gJRcpKJdhVeykc/odumyQ== BW99mCrQI0FFWb5GKOhEwduVhoXmAPoL {"id":"7f8fcdb2-573a-547a-901e-4c34f9761293","create_time":"2023-10-11T22:40:46+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"8TKBc3NQAPapdskLACAFRX2W+g5TyrEihNRIVZdJau79OJjYU/HQjRzzx8uRKM/iDuwLVWfyW028KpHcEqtNvk27v4SAwKVlMQeJJdcqa56SVdSgTzEfWE6TQsY/EzH2/z0pDuLhQ90lej3WMHpQSPMuL4tC2rdxcH5347UZlGdjzaCAg45a0RIH/RWup7hyRBG2glXZJH9Oz5yaGVSvhkXJtcz1YyTVJKyU1kPUfKuXMX/QcNQ4wTs6Zy3mcui/D4Jwu0Fv7iJ48D2EIo+S7lgilsJGl9mBtKDQdmKWMcayfv+Sd2RUN/x5Wd2B7D1rCSiP+sneG9IZ2qss6YzgekCac3ceHZ3YJeHJYACvLGdh4RciMb3/SEXxsTwQ4SeenHSmxAPCkFbf31CBOKJpDcSgeNSMKUQ0fbuue3+ccgpt65Cs4SMUiHZEGmeMnYEIozysMEhWsecbusE8UEHrEsigT4h2QtK2GEtKVtXy05tnk3cPkgFHt6PKsjMi9ii+RZ3AnBGzX12BfKlEj6xzQDWO8DYaBHU4UHCeKi1yy/I1Van5Rogn","associated_data":"transaction","nonce":"4qkJp4DV2urE"}} ] sign[1697037292]