微信推出了平台证书的替代公钥方案

对接微信v3支付改版了,按证书的方式运行,结果在构建商户配置的时候报错了,微信那边也没给具体错误信息,问了客服才知道:由证书验证商户身份改成公钥验证身份!以下是技术客服原话:

微信推出了平台证书的替代公钥方案,两者使用场景完全相同,但公钥不会过期,后续新申请的商户号将使用公钥进行验签与敏感信息加密。
微信支付公钥使用介绍:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/wxp-pub-key-guide.html
平台证书切换微信支付公钥指引:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/update-pub-key.html

新申请的商户不能再用平台证书,请申请公钥

对接SDK的代码要改成如下:

以上的意思是说,对接微信支付,有两种方式,一种是自己封装参数,自己加密解密。而另一种方式就简便很多了,Maven引入微信支付对应jar包,然后调用相应接口即可。上图是对象调用方法前,对这个对象初始化。

还不明白参考下面的,配好就能使用(除了需要商户相关资料,还需要在公众号和商户各自后台配置合法域名啊,白名单,商户关联公众号这些,这些都配置好后,下面的代码就能用)

我对接SDK的方式如下:

1、Maven引入

<!-- 微信支付apiv3版本 -->
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-java</artifactId>
            <version>0.2.14</version>
        </dependency>

 2、springboot的yml配置

wx:
  #公众号
  public:
    appId: wx556****
    sercet: 3******
    redirectUri: https://******/ #后面要拼项目名和具体接口名
  #企业微信
  corporation:
    corpId: ww**** #企业ID
    agentId: 100**** #自建应用ID
    corpsecret: nzmw0JmTF***** #自建应用密钥

  #微信支付 ,本系统示例地址:https://****/terminal/static/pay.html?myCallBack=1
  pay:
    #合作伙伴(服务商)
    partner:
      merchantId: 1314*** #商户号
      privateKeyPath: D:/hwj/aaa/普通商户1314*****/apiclient_key.pem #商户API私钥路径
      apiV3Key: 64Thi56nWE****** #商户APIV3密钥
      merchantSerialNumber: 194EB00A******* #商户证书序列号
      notifyUrl: https://*******/  #微信支付回调地址
      sub_mchid: 160***** #子商户号
    #普通商户
    general:
      merchantId: 13****** #商户号
      privateKeyPath: D:/hwj/aaa/普通商户13*****/apiclient_key.pem  #商户API私钥路径
      apiV3Key: 6reo35WVjr****** #商户APIV3密钥
      merchantSerialNumber: 6D519C633E**** #商户证书序列号
      notifyUrl: https://****/  #微信支付回调地址
      publicKeyId: PUB_KE****** #公钥ID
      publicKeyFromPath: D:/****/pub_key.pem #公钥路径

3、普通商户支付工具类(本文要讲的重点如下:v3新版和旧版)

package com.yema.utils;

import com.alibaba.fastjson.JSON;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 微信普通商户支付工具类
 * @ClassName WXPayUtil
 * @Description TODO
 * @Author orison
 * @Date 2024-08-12 17:09
 * @Version 1.0
 **/
@Component
@Slf4j
public class WXPayGeneralUtil implements ApplicationContextAware {
    /** 应用Id,如公众号、小程序等 */
    public static String appId;
    /** 商户号 */
    public static String merchantId;

    /** 商户API私钥路径 */
    public static String privateKeyPath;

    /** 商户证书序列号 */
    public static String merchantSerialNumber;

    /** 商户APIV3密钥 */
    public static String apiV3Key;

    /** 支付回调地址 */
    public static String notifyUrl;

    /**
     * 公钥路径
     * 【微信推出了平台证书的替代公钥方案,两者使用场景完全相同,但公钥不会过期,后续新申请的商户号将使用公钥进行验签与敏感信息加密。
     * 微信支付公钥使用介绍:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/wxp-pub-key-guide.html
     * 平台证书切换微信支付公钥指引:https://pay.weixin.qq.com/docs/merchant/products/platform-certificate/update-pub-key.html
     * 新申请的商户不能再用平台证书,请申请公钥】
     * */
    public static String publicKeyFromPath;
    /** 公钥ID */
    public static String publicKeyId;



    /** 项目名 */
    public static String contextPath;

    public static JsapiService service;//jsapi支付服务
    public static JsapiServiceExtension jsapiServiceExtension; //jsapi支付服务增强接口(强烈推荐)
    public static NotificationConfig notificationConfig;//回调配置
    public static RefundService refundService;//退款服务

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        Environment environment = applicationContext.getEnvironment();
        appId = environment.getProperty("wx.public.appId");
        merchantId = environment.getProperty("wx.pay.general.merchantId");
        merchantSerialNumber = environment.getProperty("wx.pay.general.merchantSerialNumber");
        privateKeyPath = environment.getProperty("wx.pay.general.privateKeyPath");
        apiV3Key = environment.getProperty("wx.pay.general.apiV3Key");
        notifyUrl = environment.getProperty("wx.pay.general.notifyUrl");
        publicKeyFromPath = environment.getProperty("wx.pay.general.publicKeyFromPath");
        publicKeyId = environment.getProperty("wx.pay.general.publicKeyId");

        contextPath = environment.getProperty("server.servlet.context-path");

        if (StringUtils.isBlank(merchantId) || StringUtils.isBlank(privateKeyPath) || StringUtils.isBlank(merchantSerialNumber) || StringUtils.isBlank(apiV3Key)){
            log.warn("由于初始化普通商户配置时缺少某些必要参数,所以不允许初始化该普通商户配置");
            return;
        }

        // 初始化商户配置
        Config config =
                null;
        try {
            if (StringUtils.isBlank(publicKeyFromPath)){
                //v3旧版
                config = new RSAAutoCertificateConfig.Builder()
                        .merchantId(merchantId)
                        // 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
                        .privateKeyFromPath(privateKeyPath)
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3Key)
                        .build();
            }else{
                //v3新版
                config = new RSAPublicKeyConfig.Builder()
                        .merchantId(merchantId)
                        .privateKeyFromPath(privateKeyPath)
                        .publicKeyFromPath(publicKeyFromPath)
                        .publicKeyId(publicKeyId)
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3Key)
                        .build();
            }


        } catch (Exception e) {
            e.printStackTrace();
        }

        // 初始化服务
        notificationConfig = (NotificationConfig)config;
        service = new JsapiService.Builder().config(config).build();
        refundService = new RefundService.Builder().config(config).build();
        jsapiServiceExtension = new JsapiServiceExtension.Builder().config(config).build();
    }



    /**
     * JSAPI 支付下单,并返回 JSAPI 调起支付数据(这些数据直接给前端唤起微信收银台的)。推荐使用!
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/direct-jsons/jsapi-prepay.html
     * <p>请求成功后,该方法返回预支付交易会话标识 prepay_id 和客户端 JSAPI 调起支付所需参数。 它相比 JsApiService.prepay
     * 更简单易用,因为无需开发者自行计算调起支付签名。
     *
     * @return PrepayWithRequestPaymentResponse
     * HttpException 发送HTTP请求失败。例如构建请求参数失败、发送请求失败、I/O错误等。包含请求信息。
     * ValidationException 发送HTTP请求成功,验证微信支付返回签名失败。
     * ServiceException 发送HTTP请求成功,服务返回异常。例如返回状态码小于200或大于等于300。
     * MalformedMessageException 服务返回成功,content-type不为application/json、解析返回体失败。
     */
    public static PrepayWithRequestPaymentResponse jsapiPrepayExtension(int total,String desc,String outTradeNo,String openId,String attach,String uri){
        PrepayRequest request = new PrepayRequest();
        // 调用request.setXxx(val)设置所需参数,具体参数可见Request定义
        Amount amount = new Amount();
        amount.setTotal(total);
        request.setAmount(amount);
        request.setAppid(appId);
        request.setMchid(merchantId);
        request.setDescription(desc); //"测试商品标题"
        request.setNotifyUrl(notifyUrl+contextPath+uri);
        request.setAttach(attach);
        request.setOutTradeNo(outTradeNo);
        Payer payer = new Payer();
        payer.setOpenid(openId);
        request.setPayer(payer);
        return jsapiServiceExtension.prepayWithRequestPayment(request);
    }


    /**
     * 根据退款单号查询退款单信息
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-out-refund-no.html
     * @param outRefundNo
     * @return
     */
    public static Result queryRefund(String outRefundNo) {
        try {
            QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
            request.setOutRefundNo(outRefundNo);
            Refund refund = refundService.queryByOutRefundNo(request);
//            status
//            可选取值:
//            SUCCESS: 退款成功
//            CLOSED: 退款关闭
//            PROCESSING: 退款处理中
//            ABNORMAL: 退款异常
            return Result.success(refund);
        } catch (ServiceException e) {
            log.error("普通商户-根据退款单号查询退款单信息异常",e);
            return Result.fail(e.getErrorMessage());
        }
    }

    /**
     * 关闭订单
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/close-order.html
     * @param outTradeNo
     * @return
     */
    public static Result closeOrder(String outTradeNo) {
        try {
            CloseOrderRequest request = new CloseOrderRequest();
            request.setMchid(merchantId);
            request.setOutTradeNo(outTradeNo);
            service.closeOrder(request);
            return Result.success();
        } catch (ServiceException e) {
            log.error("普通商户-关闭订单异常",e);
            return Result.fail(e.getErrorMessage());
        }
    }
    /**
     * 根据系统业务单号查询订单
     * (用于防止订单支付后微信没有回调支付接口,导致订单还是处于待支付,解决方案如:定时任务查询当天处于待支付订单)
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-out-trade-no.html
     * @param outTradeNo
     *
     * trade_state
     * 【交易状态】 交易状态,枚举值:
     * * SUCCESS:支付成功
     * * REFUND:转入退款
     * * NOTPAY:未支付
     * * CLOSED:已关闭
     * * REVOKED:已撤销(仅付款码支付会返回)
     * * USERPAYING:用户支付中(仅付款码支付会返回)
     * * PAYERROR:支付失败(仅付款码支付会返回)
     */
    public static Result queryOrderByOutTradeNo(String outTradeNo){
        try {
            QueryOrderByOutTradeNoRequest query = new QueryOrderByOutTradeNoRequest();
            query.setMchid(merchantId);
            query.setOutTradeNo(outTradeNo);
            Transaction transaction = service.queryOrderByOutTradeNo(query);
            return Result.success(transaction);
        } catch (ServiceException e) {
            log.error("普通商户-根据系统业务号查询订单异常",e);
            return Result.fail(e.getErrorMessage());
        }
    }


    /**
     * 微信支付单号查询订单
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-wx-trade-no.html
     * @param transactionId 【微信支付订单号】 微信支付系统生成的订单号
     * @return
     */
    public static Transaction queryOrderByTransactionId(String transactionId) {
        QueryOrderByIdRequest request = new QueryOrderByIdRequest();
        request.setMchid(merchantId);
        request.setTransactionId(transactionId);
        return service.queryOrderById(request);
    }


    /**
     * 退款
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/create.html
     * 当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
     * 注意:
     * 交易时间超过一年的订单无法提交退款(按支付成功时间+365天计算)
     * 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
     * 请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次
     * 每个支付订单的部分退款次数不能超过50次
     * 如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
     * 申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
     * 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
     * 一个月之前的订单申请退款频率限制为:5000/min
     * 同一笔订单多次退款的请求需相隔1分钟
     * @param outTradeNo
     */
    public static Result refunds(String outTradeNo,long total,long refund,String uri) {
        try {
            String symbol = StringUtils.join(WXPayGeneralUtil.randomNum(6, 10).toArray(), ",").replace(",", "");
            String str = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + symbol;
            String out_refund_no = str;//商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
            CreateRequest request = new CreateRequest();
            request.setOutTradeNo(outTradeNo);
            request.setOutRefundNo(out_refund_no);
            request.setNotifyUrl(notifyUrl+contextPath+uri);
            AmountReq amountReq = new AmountReq();
            amountReq.setRefund(refund);
            amountReq.setTotal(total);
            amountReq.setCurrency("CNY");
            request.setAmount(amountReq);
            refundService.create(request);
            return Result.success("已申请退款",out_refund_no);
        } catch (ServiceException e) {
            log.error("普通商户-退款",e);
            return Result.fail(e.getErrorMessage());
        }
    }

    /**
     * 对退款回调预处理
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html
     * @param request
     * @param response
     * @return
     */
    public static Map refundsCallbackHandle(HttpServletRequest request, HttpServletResponse response){
        try {
            //获取报文
            String body = IOUtils.toString(request.getInputStream(), "UTF-8");
            //随机串
            String nonceStr = request.getHeader("Wechatpay-Nonce");
            //微信传递过来的签名
            String signature = request.getHeader("Wechatpay-Signature");
            //证书序列号(微信平台)
            String serialNo = request.getHeader("Wechatpay-Serial");
            //时间戳
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            // 签名方式
            String signType = request.getHeader("Wechatpay-Signature-Type");
            // 构造 RequestParam
            com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
                    .serialNumber(serialNo)
                    .nonce(nonceStr)
                    .signature(signature)
                    .timestamp(timestamp)
                    .signType(signType)
                    .body(body)
                    .build();
            // 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用  config
            // 初始化 NotificationParser
            NotificationParser parser = new NotificationParser(notificationConfig);
            // 验签、解密并转换成 Transaction
            Map transaction = parser.parse(requestParam, Map.class);
            return transaction;
        } catch (Exception e) {
            e.printStackTrace();
            myWriter(response,500);
            log.error("微信支付回调Exception",e);
        }
        return null;
    }


    /**
     * 生成指定范围的随机数字
     * @param scope 需要生成的随机数字的个数
     * @param total 数字范围
     * @return
     */
    public static List<Integer> randomNum(int scope, int total) {
        List<Integer> mylist = new ArrayList<>(); // 用于储存不重复的随机数
        Random rd = new Random();
        while (mylist.size() < scope) {
            int myNum = rd.nextInt(total);
            if (!mylist.contains(myNum += 1)) { // 判断容器中是否包含指定的数字
                mylist.add(myNum); // 往集合里面添加数据。
            }
        }
        return mylist;
    }

    /**
     * 对支付回调预处理
     * 参考文档:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/payment-notice.html
     * @param request
     * @param response
     * @return
     */
    public static Map paymentCallbackHandle(HttpServletRequest request, HttpServletResponse response){
        try {
            //获取报文
            String body = IOUtils.toString(request.getInputStream(), "UTF-8");
            //随机串
            String nonceStr = request.getHeader("Wechatpay-Nonce");
            //微信传递过来的签名
            String signature = request.getHeader("Wechatpay-Signature");
            //证书序列号(微信平台)
            String serialNo = request.getHeader("Wechatpay-Serial");
            //时间戳
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            // 签名方式
            String signType = request.getHeader("Wechatpay-Signature-Type");
            // 构造 RequestParam
            com.wechat.pay.java.core.notification.RequestParam requestParam = new com.wechat.pay.java.core.notification.RequestParam.Builder()
                    .serialNumber(serialNo)
                    .nonce(nonceStr)
                    .signature(signature)
                    .timestamp(timestamp)
                    .signType(signType)
                    .body(body)
                    .build();
            // 如果已经初始化了 RSAAutoCertificateConfig,可以直接使用  config
            // 初始化 NotificationParser
            NotificationParser parser = new NotificationParser(notificationConfig);
            // 验签、解密并转换成 Transaction
            Map transaction = parser.parse(requestParam, Map.class);

            return transaction;
        } catch (Exception e) {
            e.printStackTrace();
            myWriter(response,500);
            log.error("微信支付回调Exception",e);
            return null;
        }
    }


    private static void myWriter(HttpServletResponse response,int code) {
        try {
            // 设置HTTP应答状态码,例如200 OK
            Map map = new HashMap();
            if (code == 200){
                response.setStatus(200);
                map.put("code","SUCCESS");
                map.put("message","成功");
            }else{
                response.setStatus(500);
                map.put("code","FAIL");
                map.put("message","失败");
            }
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html; charset=utf-8");
            PrintWriter writer = response.getWriter();
            String resultStr = JSON.toJSONString(map);
            writer.print(resultStr);
            writer.close();
        } catch (IOException e) {
            log.error("Exception",e);
        }
    }
}

### 使用微信支付公钥进行验签的方法 在处理来自微信支付平台的消息时,为了确保消息的真实性和完整性,需要使用微信支付平台提供的公钥来验证签名。具体过程如下: 当接收到微信支付平台发送的数据包时,其中包含了由微信支付平台私钥生成的签名字符串。接收方(即商户端)需利用事先获取到的微信支付平台证书中的公钥对该签名进行校验。 #### C# 中实现验签逻辑 对于采用C#.NET框架的应用程序而言,在完成必要的环境配置之后,可以按照以下方式编写用于执行验签操作的函数[^2]: ```csharp using System; using System.Security.Cryptography.X509Certificates; public class WeChatPayValidator { private readonly string _platformPublicKey; // 微信支付平台公钥 public bool VerifySignature(string message, string signature){ var rsa = new RSACryptoServiceProvider(); try{ var cert = new X509Certificate2(Convert.FromBase64String(_platformPublicKey)); rsa.ImportParameters(cert.GetRSAPublicKey().ExportParameters(false)); byte[] dataBytes = Encoding.UTF8.GetBytes(message); byte[] signBytes = Convert.FromBase64String(signature); SHA256 sha256 = SHA256.Create(); byte[] hashData = sha256.ComputeHash(dataBytes); return rsa.VerifyHash(hashData, signBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } finally{ rsa.Clear(); } } } ``` 此代码片段展示了如何加载微信支付平台公钥并创建`RSA`对象实例;接着计算待验证数据的实际哈希值并与传入参数中的已知签名做对比,最终返回两者是否匹配的结果。 #### Java 中实现验签逻辑 而在基于Java语言开发的服务端应用里,则可以通过引入官方支持库或者第三方工具类来进行相同的操作。这里给出一个简单的例子说明如何调用微信支付API V3所提供的方法完成验签工作[^3]: ```java import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier; import java.security.PublicKey; // 假设已经获得了WechatPayHttpClientBuilder builder 和 AutoUpdateCertificatesVerifier verifier 的实例化对象 builder.verifier(verifier); // 获取当前使用的公钥 PublicKey publicKey = ((AutoUpdateCertificatesVerifier)verifier).getPublicKeys().values().iterator().next(); // 进行验签流程... boolean isValid = Signature.getInstance("SHA256withRSA").verify(/*...*/); ``` 上述两段代码分别演示了不同编程环境中怎样运用各自特性去达成同样的目标——通过给定的信息以及微信支付平台公开发布的公钥来确认所收到来自该平台的通知或响应的有效性。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值