花了一周左右的时间使用微信支付APIV3实现微信支付,以前也没做过微信支付,现在项目上线有点时间就把知识记录分享出来。

第三代微信支付 apiv3

1. 前言

apiV3之前的版本:

微信支付有五个基础步骤:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API

2、商户server调用支付统一下单,api参见公共api【统一下单API

3、商户server调用再次签名,api参见公共api【再次签名

4、商户server接收支付通知,api参见公共api【支付结果通知API

5、商户server查询支付结果,api参见公共api【查询订单API

2. 介绍

2.1 描述

微信支付APIV3

微信官方介绍:

为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。

相较于的之前微信支付API,主要区别是:

  • 遵循统一的Restful的设计风格
  • 使用JSON作为数据交互的格式,不再使用XML
  • 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256
  • 不再要求HTTPS客户端证书
  • 使用AES-256-GCM,对回调中的关键信息进行加密保护

2.2 整体结构

开发指引

第三方支付平台java开发 第三方支付平台api_API

apiV3实现微信支付

步骤一 用户下单发起支付,商户可通过微信支付《APP下单JSAPI支付Native下单H5下单》创建支付订单。

步骤二 商户通过小程序《APP调起支付JS调起支付Native调起支付H5调起支付》调起微信支付,发起支付请求。

步骤三 用户支付成功后,商户可接收到微信支付支付结果通知《支付结果通知》。

步骤四 商户也可主动调用《查询订单》查询支付结果。

2.3 官方SDK和工具(可选)

工具的作用是协助你完成固定的工作。但是有个前提,你得花时间去学习这个工具。

2.3.1 SDK

官方开源了两套(Java、Php)协助商户工程师接入微信支付平台的工具包。里面代码并不多,完全可以阅读拿下,并且可以更规范签名、加密、解密等一系列操作,避免不必要的时间消耗。我个人是更推荐大家去看一遍自己语言对应的sdk再进行开发,哪怕你不用。

2.3.2 工具

如果开发者使用过PostmanAPI的调试,我们推荐在正式开发之前,使用Postman签名脚本 进行接口体验

另外,我们提供的微信支付平台证书下载工具,可以协助开发者完成证书下载。同时,它也是个很好的Java示例程序。

2.3.3 第三方工具或库

签名/验签/加密/解密工具(点击下载)

如果自行实现验证平台签名逻辑的话,需要注意以下事项:

  • 该微信支付没有审核或者控制以下的第三方工具和库,不能保证它们的安全性和可靠性。

3. 开发

微信支付基础知识(必会)

什么是密钥/证书:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay7_0.shtml

懂得一些基础知识,会让你开发的思路更顺畅。

3.1 准备材料

  • 商户证书文件:apiclient_cert.p12
  • ApiV3 密钥:商户需先在【商户平台】->【API安全】的页面设置该密钥,请求才能通过微信支付的签名校验。密钥的长度为32个字节
  • 商户号mchid
  • 公众号appid

3.2 步骤一:下单

3.2.1 思路
  1. 自己商城平台的订单创建,产生"商户订单编号";
  2. **(难点)**打包参数,请求微信支付平台下单接口,并成功获得预下单编号;
    下单api:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
  3. 打包小程序调起支付需要的参数,主要:用户openid、预下单编号、商户编号;
    调起支付参数:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
  4. 返回前端;
3.2.2 微信下单代码:
// 微信支付下单 begin
        Map<String,Object> vxParm = new HashMap<>();
        vxParm.put("appid",payWxPayAppid);
        vxParm.put("mchid",payWxpayMchid);
        vxParm.put("description",virtualGoods.getGdName());//商品描述
        vxParm.put("out_trade_no",virtualGoodsOdr.getOdrNum());//商户订单号 string[6,32] 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一 示例值:1217752501201407033233368018
//        vxParm.put("time_expire","");//非必填   订单失效时间
//        vxParm.put("attach","");//非必填  附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
        vxParm.put("notify_url", serverHostname+notifyUrl);//通知URL必须为直接可访问的URL,不允许携带查询串。
//        vxParm.put("goods_tag","");//非必填    订单优惠标记
        Map<String,Object> amount = new HashMap<>();//订单金额信息
        amount.put("total",((Double)(virtualGoods.getPrice()*100)).intValue());//订单总金额,单位为分。
        amount.put("currency","CNY");//非必填 CNY:人民币,境内商户号仅支持人民币。
        vxParm.put("amount",amount);
        Map<String,Object> payer = new HashMap<>();//支付者信息
        payer.put("openid",openId);//用户在直连商户appid下的唯一标识。 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
        vxParm.put("payer",payer);
        WechatPayHttpClientBuilder builder = null;
        PrivateKey privateKey = null;
        try {
            X509Certificate certificate = (X509Certificate) SpringContextUtil.getBean("certificate");
            privateKey = (PrivateKey) SpringContextUtil.getBean("privateKey");

            builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(payWxpayMchid, certificate.getSerialNumber().toString(16).toUpperCase(), privateKey)
                    .withValidator(new WechatPay2Validator(autoUpdateCertificatesVerifier));

        } catch (Exception e) {
            e.printStackTrace();
            log.info("【{}】读取证书失败:{}", logTitle, e.getMessage());
        }

        JSONObject responseMap =  JSON.parseObject(HttpUtil.vxPayPost(placeOrderUrl, JSON.toJSONString(vxParm), builder.build()));
        //预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时 示例值:wx201410272009395522657a690389285100
        String prepay_id;
        if((prepay_id = (String)(responseMap).get("prepay_id"))==null){
            log.info("【{}】下单失败:{}", logTitle,responseMap.get("message"));
            throw new BusinessException("下单失败,"+responseMap.get("message"));
        }
        // 微信支付下单 end

小结:

构建好下单请求的签名,获得prepay_id预下单编号。

签名和认证信息比较麻烦。

3.2.3 构建前端支付参数
// 微信支付参数 begin
        Map<String,Object> payParm = new HashMap<>();
        rsl.put("payParm", payParm);
        payParm.put("appId", payWxPayAppid);
        payParm.put("timeStamp", System.currentTimeMillis()/1000+"");
        payParm.put("nonceStr", UUID.randomUUID().toString().replaceAll("-","")); // .toUpperCase()
        payParm.put("package", "prepay_id="+prepay_id);
        payParm.put("signType", "RSA");
        StringBuffer paySign = new StringBuffer(payWxPayAppid+"\n")
                .append(payParm.get("timeStamp")+"\n")
                .append(payParm.get("nonceStr")+"\n")
                .append(payParm.get("package")+"\n");
        try {
//            PrivateKeySigner privateKeySigner = new PrivateKeySigner(certificate.getSerialNumber().toString(16).toUpperCase(), privateKey);
//            privateKeySigner.sign()

        //私钥签名并Base64编码
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(privateKey);
            sign.update(paySign.toString().getBytes());
            payParm.put("paySign", Base64.getEncoder().encodeToString(sign.sign()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 微信支付参数 end

小结:这里的参数是前端真实调起支付时用的参数。

需要特别注意的是 appId 等参数的大小写,一定要对照官网说明的格式,不少人在这里苦找半天。

3.3 步骤二:支付通知

通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml

3.3.1 思路
  1. (麻烦)验证通知接口是否来自微信支付平台;
  2. 使用微信支付平台公钥解析通知报文;
  3. 修改商户平台订单的状态为已支付;
3.3.2 验证通知

从头部获取序列号、签名、请求主体、时间戳、随机字符串

tring wechatpaySerial = request.getHeader("Wechatpay-Serial");
String wechatpaySingature = request.getHeader("Wechatpay-Signature");
String noticeBody = SysContexts.getRequestParameterMap().get("BODY").toString();
String wechatpayTimestamp = request.getHeader("Wechatpay-Timestamp");
String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
StringBuffer message = new StringBuffer(wechatpayTimestamp+"\n")
    .append(wechatpayNonce+"\n")
    .append(noticeBody+"\n");

// 通知验签
if(autoUpdateCertificatesVerifier.verify(wechatpaySerial, message.toString().getBytes(), wechatpaySingature)){
    
}

说明:autoUpdateCertificatesVerifier是微信支付平台提供的sdk,可以自动获取平台证书并提供验证的入口

小结:

特别注意的是获取请求主体,因为推送过来的请求是通过post方法,参数以json形式传送,所以使用流读取的body。也就是说只有第一次读取时才有值,后面再怎么get都是空,这将直接导致验签失败。

3.3.3 解析报文

noticeBody为报文主体。

具体参数我封装在对象里。可以参考官方api:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml

//-------------------- 解析通知 begin
VxNoticeVo vxNoticeVo = JSON.parseObject(noticeBody, VxNoticeVo.class);
VxNoticeVo.Resource resource = vxNoticeVo.getResource();
String Ciphertext = "";
//目前微信提供的只有AEAD_AES_256_GCM算法
if("AEAD_AES_256_GCM".equals(resource.getAlgorithm())){
    WxAPIV3AesUtil wxAPIV3AesUtil = new WxAPIV3AesUtil(apiV3Key.getBytes());
    try {
        Ciphertext = wxAPIV3AesUtil.decryptToString(resource.getAssociated_data().getBytes(), resource.getNonce().getBytes(), resource.getCiphertext());
    } catch (GeneralSecurityException | IOException e) {
        log.info("【{}】vxNotice():Ciphertext解析失败。通知类型:{},通知数据类型:{},通知内容:{}", logTitle, vxNoticeVo.getEvent_type(),vxNoticeVo.getResource_type(), vxNoticeVo.getSummary());
        e.printStackTrace();
    }
}
//-------------------- 解析通知 end

小结

这里是整个支付过程最简单的部分了。解析后的内容格式为json,可以直接转对象。

3.4 查询订单、申请退款、退款通知

下面这三块功能暂时没有业务需要,后续我实现了再补充具体感受。应该跟上面的操作大同小异了。

官方api:

查询订单:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_2.shtml

申请退款:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_9.shtml

退款通知:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_11.shtml