微信支付V3提示支付签名验证失败

微信V3支付时总是提示支付签名验证失败,后端是java 前端是uniapp开发小程序
前端代码

    toRecharge() {
                let list = {
                    openid: this.openid,
                    userId: this.userInfo.id,
                    rechargeAmount: 0.1,
                    type: 'sub_jsapi'
                }
                transactions(list).then((res1) => {
                    //成功结果
                    console.log(res1)
                    let params = res1.data.signMap
                    console.log(params)
                    uni.requestPayment({
                        provider: 'wxpay',
                        timeStamp: params.timeStamp,
                        nonceStr: params.nonceStr,
                        package: params.package,
                        orderInfo:"充值",
                        signType: params.signType,
                        paySign: params.paySign,
                        sign: params.paySign,
                        success: (res) => {
                            
                            uni.showToast({
                                title: '支付成功'
                            })
                        },
                        fail: (err) => {
                            console.log(err)
                        
                        },
                        complete: (err) => {
                            console.log(err)
                            //this.getOrderStatus()
                            this.btnDisabled = false
                        }
                    });
                }).catch(err => {
                    //失败结果
                    console.log(32323232)
                    console.log(err)
                });
            },

后端代码


package com.xjc.vueapi.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xjc.common.config.WechatPayConfig;
import com.xjc.common.config.WechatPayUrlEnum;
import com.xjc.common.request.WechatPayRequest;
import com.xjc.common.untils.ResultUtils;
import com.xjc.entity.RechargeRecord;
import com.xjc.service.RechargeRecordService;
import com.xjc.service.WeChatUserService;
import com.xjc.vo.WeChatPayVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @ClassName WeChatPayController
 * @Description
 * @Author bill
 * @Date 2022-07-20 16:19
 **/
@RestController
@RequestMapping("wx/pay")
@RequiredArgsConstructor
@Slf4j
public class WeChatPayController {
    final WechatPayConfig wechatPayConfig;
    final WechatPayRequest wechatPayRequest;
    final WeChatUserService weChatUserService;

    final RechargeRecordService rechargeRecordService;

    /**
     * 无需应答签名
     */
    final CloseableHttpClient wxPayNoSignClient;

    /**
     * 交易
     * @param type h5、jsapi、app、native、sub_jsapi
     * @return
     */
    @RequestMapping("transactions")
    public ResultUtils transactions(@RequestBody WeChatPayVO weChatPayVO){
        if (weChatPayVO.getUserId()== null){
            return new ResultUtils("203","请先登录");
        }
        if(StringUtils.isBlank(weChatPayVO.getOpenid())){
            return new ResultUtils("203","请先登录");
        }

        //先判断是否存在
        Long userId = weChatUserService.selectUserIdByOpenid(weChatPayVO.getOpenid());
        if(!weChatPayVO.getUserId().equals(userId)){
            return new ResultUtils("203","请先登录");
        }
        if(weChatPayVO.getRechargeAmount()<=0){
            return new ResultUtils("203","充值金额不能小于0");
        }
        //添加记录
        RechargeRecord rechargeRecord = new RechargeRecord();
        rechargeRecord.setUserId(weChatPayVO.getUserId());
        rechargeRecord.setRechargeAmount(weChatPayVO.getRechargeAmount());
        rechargeRecord.setRechargeMethod(weChatPayVO.getType());
        rechargeRecordService.insertRecords(rechargeRecord);
        // 统一参数封装
        Map params = new HashMap<>(8);
        params.put("appid", wechatPayConfig.getAppId());
        params.put("mchid", wechatPayConfig.getMerchantId());
        params.put("description", "充值信息");
        int outTradeNo = new Random().nextInt(999999999);
        params.put("out_trade_no", outTradeNo+"");
        params.put("notify_url", wechatPayConfig.getNotifyOrderUrl());

        Map amountMap = new HashMap<>(4);
        // 金额单位为分
        amountMap.put("total", 1);
        amountMap.put("currency", "CNY");
        params.put("amount", amountMap);

        // 场景信息
        Map sceneInfoMap = new HashMap<>(4);
        // 客户端IP
        sceneInfoMap.put("payer_client_ip", "127.0.0.1");
        // 商户端设备号(门店号或收银设备ID)
        sceneInfoMap.put("device_id", "127.0.0.1");

        // 除H5与JSAPI有特殊参数外,其他的支付方式都一样
        if (weChatPayVO.getType().equals(WechatPayUrlEnum.H5.getType())) {

            Map h5InfoMap = new HashMap<>(4);
            // 场景类型:iOS, Android, Wap
            h5InfoMap.put("type", "IOS");
            sceneInfoMap.put("h5_info", h5InfoMap);
        } else if (weChatPayVO.getType().equals(WechatPayUrlEnum.JSAPI.getType()) || weChatPayVO.getType().equals(WechatPayUrlEnum.SUB_JSAPI.getType())) {
            Map payerMap = new HashMap<>(4);
            payerMap.put("openid", weChatPayVO.getOpenid());
            params.put("payer", payerMap);
        }

        params.put("scene_info", sceneInfoMap);

        String paramsStr = JSON.toJSONString(params);
        log.info("请求参数 ===> {}" + paramsStr);

        // 重写type值,因为小程序会多一个下划线(sub_type)
        String[] split = weChatPayVO.getType().split("_");
        String  newType = split[split.length - 1];
        String url = wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.PAY_TRANSACTIONS.getType().concat(newType));
        log.info(url);
        String resStr = wechatPayRequest.wechatHttpPost(wechatPayConfig.getBaseUrl().concat(WechatPayUrlEnum.PAY_TRANSACTIONS.getType().concat(newType)), paramsStr);

        Map resMap = JSONObject.parseObject(resStr, new TypeReference>(){});

        Map signMap = paySignMsg(resMap, weChatPayVO.getType());
        resMap.put("type",weChatPayVO.getType());
        resMap.put("signMap",signMap);
        return new ResultUtils(resMap);
    }
    private  Map  paySignMsg(Map map,String type){
        // 设置签名信息,Native与H5不需要
        if(type.equals(WechatPayUrlEnum.H5.getType()) || type.equals(WechatPayUrlEnum.NATIVE.getType()) ){
            return null;
        }

        long timeMillis = System.currentTimeMillis();
        String appId = wechatPayConfig.getAppId();
        String timeStamp = timeMillis/1000+"";
        String nonceStr = timeMillis+"";
        String prepayId = map.get("prepay_id").toString();
        String packageStr = "prepay_id="+prepayId;

        // 公共参数
        Map resMap = new HashMap<>();
        resMap.put("nonceStr",nonceStr);
        resMap.put("timeStamp",timeStamp);

        // JSAPI、SUB_JSAPI(小程序)
        if(type.equals(WechatPayUrlEnum.JSAPI.getType()) || type.equals(WechatPayUrlEnum.SUB_JSAPI.getType()) ) {
            resMap.put("appId",appId);
            resMap.put("package", packageStr);
            // 使用字段appId、timeStamp、nonceStr、package进行签名
            String paySign = createSign(resMap);
            resMap.put("paySign", paySign);
            resMap.put("signType", "MD5");
        }
        // APP
        if(type.equals(WechatPayUrlEnum.APP.getType())) {
            resMap.put("appid",appId);
            resMap.put("prepayid", prepayId);
            // 使用字段appId、timeStamp、nonceStr、prepayId进行签名
            String sign = createSign(resMap);
            resMap.put("package", "Sign=WXPay");
            resMap.put("partnerid", wechatPayConfig.getMerchantId());
            resMap.put("sign", sign);
            resMap.put("signType", "HMAC-SHA256");
        }
        return resMap;
    }
    /**
     * 获取加密数据
     */
    private  String createSign(Map params){
        try {
            Map treeMap = new TreeMap<>(params);
            List signList = new ArrayList<>(5);
            for (Map.Entry entry : treeMap.entrySet())
            {
                signList.add(entry.getKey() + "=" + entry.getValue());
            }
            String signStr = String.join("&", signList);

            signStr = signStr+"&key="+wechatPayConfig.getV3Key();
            System.out.println(signStr);

            Mac sha = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(wechatPayConfig.getV3Key().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            sha.init(secretKey);
            byte[] array = sha.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
            }
            signStr = sb.toString().toUpperCase();
            System.out.println(signStr);

            return signStr;
        }catch (Exception e){
            throw new RuntimeException("加密失败!");
        }
    }
}


WechatPayConfig如下

package com.xjc.common.config;

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName WechatPayConfig
 * @Description
 * @Author bill
 * @Date 2022-07-20 15:08
 **/
@Component
@Data
@Slf4j
@ConfigurationProperties(prefix = "wx")
public class WechatPayConfig {
        /**
         * 小程序ID
         */
        private String appId;
        /**
         * 小程序密钥
         */
        private String appSecret;
        /**
         * 商户id
         */
        private String merchantId;
        /**
         * 证书序列号
         */
        private String merchantSerialNumber;
        /**
         * v3密钥
         */
        private String v3Key;
        /**
         * 下单成功后回调
         */
        private String notifyOrderUrl;
        /**
         * 退款回调url
         */
        private String notifyRefoundUrl;
        private String keyPath;
        private String certPath;
        /**
         * 微信支付V3-url前缀
         */
        private String baseUrl;
        /**
         * // 定义全局容器 保存微信平台证书公钥
         */
        public Map certificateMap = new ConcurrentHashMap<>();
        /**
         * 获取商户的私钥文件
         * @param keyPemPath
         * @return
         */
        public PrivateKey getPrivateKey(String keyPemPath){

                InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
                if(inputStream==null){
                        throw new RuntimeException("私钥文件不存在");
                }
                return PemUtil.loadPrivateKey(inputStream);
        }
        /**
         * 获取证书管理器实例
         * @return
         */
        @Bean
        public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {

                log.info("获取证书管理器实例");

                //获取商户私钥
                PrivateKey privateKey = getPrivateKey(keyPath);

                //私钥签名对象
                PrivateKeySigner privateKeySigner = new PrivateKeySigner(merchantSerialNumber, privateKey);

                //身份认证对象
                WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(merchantId, privateKeySigner);

                // 使用定时更新的签名验证器,不需要传入证书
                CertificatesManager certificatesManager = CertificatesManager.getInstance();
                certificatesManager.putMerchant(merchantId,wechatPay2Credentials,v3Key.getBytes(StandardCharsets.UTF_8));

                return certificatesManager.getVerifier(merchantId);
        }
        /**
         * 获取支付http请求对象
         * @param verifier
         * @return
         */
        @Bean(name = "wxPayClient")
        public CloseableHttpClient getWxPayClient(Verifier verifier)  {

                //获取商户私钥
                PrivateKey privateKey = getPrivateKey(keyPath);

                WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                        .withMerchant(merchantId, merchantSerialNumber, privateKey)
                        .withValidator(new WechatPay2Validator(verifier));

                // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
                return builder.build();
        }
        /**
         * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
         */
        @Bean(name = "wxPayNoSignClient")
        public CloseableHttpClient getWxPayNoSignClient(){

                //获取商户私钥
                PrivateKey privateKey = getPrivateKey(keyPath);

                //用于构造HttpClient
                WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                        //设置商户信息
                        .withMerchant(merchantId, merchantSerialNumber, privateKey)
                        //无需进行签名验证、通过withValidator((response) -> true)实现
                        .withValidator((response) -> true);

                // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
                return builder.build();
        }

}


WechatPayRequest 如下


package com.xjc.common.request;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;

/**
 * @ClassName WechatPayRequest
 * @Description 封装统一请求处理
 * @Author bill
 * @Date 2022-07-20 16:03
 **/
@Component
@Slf4j
public class WechatPayRequest {
    @Resource
    private CloseableHttpClient wxPayClient;
    public  String wechatHttpGet(String url) {
        try {
            // 拼接请求参数
            HttpGet httpGet = new HttpGet(url);
            httpGet.setHeader("Accept", "application/json");

            //完成签名并执行请求
            CloseableHttpResponse response = wxPayClient.execute(httpGet);

            return getResponseBody(response);
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
    }

    public  String wechatHttpPost(String url,String paramsStr) {
        try {
            HttpPost httpPost = new HttpPost(url);
            StringEntity entity = new StringEntity(paramsStr, "utf-8");
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept", "application/json");

            CloseableHttpResponse response = wxPayClient.execute(httpPost);
            return getResponseBody(response);
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }
    }

    private String getResponseBody(CloseableHttpResponse response) throws IOException {

        //响应体
        HttpEntity entity = response.getEntity();
        String body = entity==null?"": EntityUtils.toString(entity);
        //响应状态码
        int statusCode = response.getStatusLine().getStatusCode();

        //处理成功,204是,关闭订单时微信返回的正常状态码
        if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
            log.info("成功, 返回结果 = " + body);
        } else {
            String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
            log.error(msg);
            throw new RuntimeException(msg);
        }
        return body;
    }
}

img

这两个都试过,一个是和apiclient_key一起下载的,另一个是通过Certificate Downloader生成的,都不行,一直提示支付签名验证失败,这是怎么回事

签名算法是有规则的。

签名生成的通用步骤如下:

第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

特别注意以下重要规则:

◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。 注意:密钥的长度为32个字节。

https://www.bilibili.com/video/BV1og411F7Uz/

APP下载地址
https://api.mch.weixin.qq.com/v3/pay/transactions/app
小程序下载地址
https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

微信支付的V3版本使用的是RSA加密,从前的V2版本使用的是MD5加密。今天在调试微信小程序的时候,始终无法调起微信支付,提示“支付签名验证失败”

问题排查思路:

  1. 先调起其它接口,如统一下单、订单查询、关单接口等,如果调起其它接口正常,则代表各种参数都正常,如证书、商户号、密钥等都正确。

统一下单接、关单、查单接口可以参考【Java教程】微信支付V3版本_哔哩哔哩_bilibili

同一商户的微信小程序的appId和APP应用的appId不同,但商户号和证书密钥等参数都相同

2.签名串需要严格区分大小写,注意字符串最后还要拼接“\n”

不同API的下单地址不同,参数有少量差别

APP下单地址:https://api.mch.weixin.qq.com/v3/pay/transactions/app

小程序下单地址:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

两种API的最后一级不同。

另外小程序获取预支付id需要用户登录的openId,且下单付款的用户必须和登录的用户是一个

APP签名串

wx8888888888888888
1414561699
5K8264ILTKCH16CQ2502SI8ZNMTM67VS
WX1217752501201407033233368018

JSAPI签名串


wx8888888888888888
1414561699
5K8264ILTKCH16CQ2502SI8ZNMTM67VS
prepay_id=wx201410272009395522657a690389285100

两种API的前三个串都是appId、时间戳、随机字符串

不同的是,app的第四个串是预支付id,而小程序的签名串需要在预支付id前写一个prepay_id=

注意能获取到预支付信息prepay_id不代表能调起应用成功
3.签名方法不同
我在调起app的时候一次就成功付款了,以至于没有怀疑过签名算法有问题,卡了四个多小时就是不知道为什么,最后终于让我发现了这个坑

在微信的官方demo里

com.wechat.pay.contrib.apache.httpclient.util.RsaCryptoUtil.encryptOAEP(String message, X509Certificate certificate)
第一次就是用这个方法给app做签名串的,没有任何问题,后来迁移到小程序就不行了,

小程序不能用这个方法,要用

String sign(byte[] message) {
    Signature sign = Signature.getInstance("SHA256withRSA");
    sign.initSign(你的私钥privateKey);
    sign.update(message);
    return Base64.getEncoder().encodeToString(sign.sign());
}

还有这个必推 :
https://m.baidu.com/from=1020761t/bd_page_type=1/ssid=0/uid=0/pu=usm%401%2Csz%40320_1001%2Cta%40iphone_2_11.0_19_11.7/baiduid=F16DE8FE16DFAE2235E549EEC8D00337/w=0_10_/t=iphone/l=1/tc?ref=www_iphone&lid=11744917947631713771&order=4&gsflag=4&fm=alop&isAtom=1&waplogo=1&clk_info=%7B%22tplname%22%3A%22www_normal%22%2C%22srcid%22%3A1599%2C%22jumpType%22%3A%22%22%7D&is_baidu=0&tj=www_normal_4_0_10_title&vit=osres&m=8&cltj=normal_title&asres=1&nt=wnor&title=%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%B8%8B%E5%8D%95APIv3%E6%97%A0%E6%B3%95%E8%B5%B0%E5%AE%8C%E6%95%B4%E4%B8%AA%E6%B5%81%E7%A8%8B%2C%E6%94%AF%E4%BB%98%E9%AA%8C%E8%AF%81%E7%AD%BE%E5%90%8D%E5%A4%B1%E8%B4%A5%3F&wd=&eqid=a2fe54a568b2bdeb1000000463085512&w_qd=IlPT2AEptyoA_yk55g9d63GuC5pTeZmYVZFupKaUu06_YcpgU8QizlCKfrc-j4BNwcEx2OdKR_&bdver=2&tcplug=1&dict=-1&sec=23525&di=cc1b1447c2d2fafb&bdenc=1&tch=124.91.250.1135.0.0&nsrc=Q7hiF3Q2MDNSdqm3E8joi5uLYRQON2LGmLxhtGG6fAp6%2FI90XV06ah%2B1yeu5V6DWtqiRns22kBOAyn%2BagfjHzOO6%2FomJUGUZFU6agzqHdRq%2BA4NysVReXm%2BZv558tq%2Fr4bQi7N4myDDYaFGkTkG1IbXrbJEZV4YCax%2FGPjGvERs%3D

提示签名失败,一般就是参数少了,或者参数加密的顺序不对,

文章:微信支付v3 签名与验签 中也许有你想要的答案,请看下吧

参考这个:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
另外微信支付问题,最好从微信社区中提问,你说的这个应该很多人遇见过,看看社区中是否有帖子.

签名验证失败一方面可能是认证的信息不对,另一方面可能是文件已失效