原创

记录APP(后台)接入微信支付APIV3

温馨提示:
本文最后更新于 2022年10月26日,已超过 919 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

一 前言

在如今的时代大背景下,手机支付成了生活中不可缺少的一环。其中的手机支付两巨头,当属微信和支付宝了,当我们想要在自己的项目吸引和留住用户,其中便捷支付必不可少,微信和支付宝支付就成为了最基础且最便捷的支付选项了。

其中支付宝支付,文档和SDK都比较简单明了,支付和回调验证都有demo进行示例,因此通过官方文档能迅速上手。微信支付相对于支付宝支付,多了很多加密解密,加签和验签的操作,流程上会比较复杂一些,因此记录一次接入微信支付APIV3的流程,第一个加深印象,其次希望能提供给其他用户一些小小的帮助。本次支付后台使用java实现。

二 接入准备

接入准备可以参考微信官方文档,APP支付接入前准备

其中需要准备好的内容如下,这些都放在了配置文件中。

  1. 商户号 merchantId

  2. API v3密钥 apiV3Key

  3. 商户API证书的证书序列号 merchantSerialNumber

    以上三项都可以在账户中心找到

  4. 商户API私钥 merchantPrivateKey

    下载到本地的商户证书文件apiclient_key.pem ,为了方便,直接存在了properties文件中,将每个换行符替换成\n,为了节约空间,省略掉中间部分如下

    -----BEGIN CERTIFICATE-----\n
    MIID8DCCAtigAwIBAgIUa7EnE8gKbMS5KaXKozPCbtsaeDwwDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTCl.........\nrq8KQU3zfyKS/TCQfiuCEfX1DKmuyaqLm28Y9+8v/mB+KKJF9NYlht+qsc63roOy\nKbeDiQ==\n-----END CERTIFICATE-----
    
  5. AppId 已上线的app对应的id

三 接入微信支付

微信支付的所有请求,都选择用其封装好的httpclient,因此需要进行httpclient配置。参考APP支付开发指引

1. 搭建和配置开发环境

1.wechatpay-apache-httpclient 版本

版本选用最新版,方便省事。

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.8</version>
</dependency>

2.httpclient配置

  1. 配置WeChatPayProperties

    @Data
    //匹配配置文件中前缀为 wechatpay 的配置项
    @ConfigurationProperties("wechatpay")
    public class WeChatPayProperties {
        /**
         * 商户号
         */
        private String merchantId;
    
        /**
         * remark:API v3密钥
         */
        private String apiV3Key;
    
        /**
         * remark:商户API证书的证书序列号
         */
        private String merchantSerialNumber;
    
        /**
         * remark:商户API私钥
         */
        private String merchantPrivateKey;
    }
    
  2. 配置httpclient

//开启WeChatPayProperties的配置功能
@EnableConfigurationProperties(WeChatPayProperties.class)
@Configuration
public class WeChatPayConfig {

    @Bean
    public CloseableHttpClient getCloseableHttpClient(WeChatPayProperties weChatPayProperties) throws IOException, HttpCodeException, GeneralSecurityException, NotFoundException {
        //该构建代码参考官网 https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(weChatPayProperties.getMerchantId(), new WechatPay2Credentials(weChatPayProperties.getMerchantId(),
                        new PrivateKeySigner(weChatPayProperties.getMerchantSerialNumber(),
                                PemUtil.loadPrivateKey(
                                        new ByteArrayInputStream(weChatPayProperties.getMerchantPrivateKey().getBytes("utf-8"))))),
                weChatPayProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));

        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(weChatPayProperties.getMerchantId());

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(weChatPayProperties.getMerchantId(),
                        weChatPayProperties.getMerchantSerialNumber(),
                        PemUtil.loadPrivateKey(
                                new ByteArrayInputStream(weChatPayProperties.getMerchantPrivateKey().getBytes("utf-8"))))
                .withValidator(new WechatPay2Validator(verifier));

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
        CloseableHttpClient httpClient = builder.build();
        return httpClient;
    }
}

2. 后台处理支付流程

只包含后台的处理流程,省略app端的支付过程。。。。

1. 生成待支付订单

下单参考APP下单API

    //获取支付订单 goodsName 商品名  orderPrice 订单价格(单位为分)  orderId 订单id  notifyUrl 回调通知的地址(本项目的)
    //在业务代码中有专门处理异常的地方,所以在生成支付订单的时候如果有异常直接抛出
    public String getWeChatPayOrderStr(String goodsName, Integer orderPrice,Long orderId,String notifyUrl) throws Exception {

        //构建请求
        URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/app");
        //构建支付参数,此处的参数都是参考下单API中的请求参数进行构建,也可以通过Map构建更简单,每项参数文档上都很清楚,此处不做过多赘述
        WechatPayAmount wechatPayAmount = new WechatPayAmount();
        wechatPayAmount.setCurrency("CNY");
        wechatPayAmount.setTotal(orderPrice);
        WechatPayParam wechatPayParam = new WechatPayParam();
        wechatPayParam.setAppid(appId);
        wechatPayParam.setMchid(merchantId);
        wechatPayParam.setDescription(goodsName);
        wechatPayParam.setNotify_url(notifyUrl);
        wechatPayParam.setOut_trade_no(orderId + "");
        wechatPayParam.setAmount(wechatPayAmount);
        StringEntity stringEntity = new StringEntity(JSON.toJSONString(wechatPayParam));
        //构件post请求
        HttpPost post = new HttpPost(uriBuilder.build());
        post.setEntity(stringEntity);
        post.addHeader("Accept", "application/json");
        post.addHeader("Content-Type", "application/json");
        //使用httpClient发起请求,将请求的返回值处理成json字符串返回
        HttpResponse execute = httpClient.execute(post);
        String bodyAsString = EntityUtils.toString(execute.getEntity());
        return bodyAsString;
    }

2. 支付订单签名

    //将支付订单处理生成签名,然后返回给app端
    public WeChatPayDto getWeChatPaySign(String bodyAsString) throws Exception {
        JSONObject jsonObject = JSON.parseObject(bodyAsString, JSONObject.class);
        //将获取到的prepay_id 进行 加密
        StringBuilder sb = new StringBuilder();
        sb.append(appId+"\n");
        //时间戳,秒  DateUtil 为hutool下的工具
        String timeStamp = DateUtil.currentSeconds() + "";
        sb.append(timeStamp + "\n");
        //随机字符串,采用的开源雪花算法ID生成工具  YitIdHelper  https://gitee.com/yitter/idgenerator
        String nonceStr = YitIdHelper.nextId() + "";
        sb.append(nonceStr + "\n");
        String repayId = jsonObject.getString("prepay_id");
        sb.append(repayId + "\n");
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey);
        // 进行签名服务
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(merchantPrivateKey);
        signature.update(sb.toString().getBytes("utf-8"));

        byte[] signedData = signature.sign();
        String signData = Base64.getEncoder().encodeToString(signedData);
        //weChatPayDto是app端调用支付需要的请求参数
        //appId partnerId(商家id)  prepayId(后台生成的待支付订单)  packageValue  nonceStr  timeStamp  sign(生成的签名)
        WeChatPayDto weChatPayDto = new WeChatPayDto();
        weChatPayDto.setAppId(WechatPayUtil.appId);
        weChatPayDto.setPartnerId(WechatPayUtil.mchId);
        weChatPayDto.setNonceStr(nonceStr);
        weChatPayDto.setPrepayId(repayId);
        weChatPayDto.setTimeStamp(timeStamp);
        weChatPayDto.setSign(signData);
        return weChatPayDto;
    }

3. 支付通知

app端支付成功后,微信会通知到我们提供的回调接口,参考支付通知 。 在项目中提供的回调接口,建议写法如下,这样写能直接获取到通知数据,不用再通过其他处理去获取通知数据。

    @ApiOperation(value = "微信支付订单回调接口")
    @PostMapping("/wechatPayCallback")
    public WeChatReply wechatPayCallback(
            HttpServletRequest request, @RequestBody Map<String,Object> body){

        return orderService.wechatPayCallback(request,body);

    }
   //WeChatReply(通知应答体)  code 和  message
3.1 通知验签
//验证签名
public boolean checkSign(HttpServletRequest request, Map<String,Object> requestBody) {
    boolean b = false;
    try {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getHeader("wechatpay-timestamp")+"\n");
        sb.append(request.getHeader("wechatpay-nonce")+"\n");
        sb.append(JSON.toJSONString(requestBody)+"\n");
        Signature signature = Signature.getInstance("SHA256withRSA");
        //此处的证书,一定要是微信支付平台的证书,否则验证不会成功。(文档已经提示过,马虎了一下就踩坑)
        signature.initVerify(getCertificates());
        signature.update(sb.toString().getBytes(StandardCharsets.UTF_8));
        String signatureFLg = request.getHeader("wechatpay-signature");
        b = signature.verify(Base64Utils.decodeFromString(signatureFLg));
    }catch (Exception e){
        log.error("校验签名出错",e);
    }
    return b;
}

    /**
     * 获取平台证书,证书的获取和处理在文档都有记录
     */
    public X509Certificate getCertificates() throws Exception {
        //从微信平台获取证书
        HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
        httpGet.setHeader("Accept", "application/json");
        //生成签名
        httpGet.setHeader("Authorization ", getSign("GET", HttpUrl.parse("https://api.mch.weixin.qq.com/v3/certificates"), ""));
        //完成签名并执行请求
        CloseableHttpResponse response = httpClient.execute(httpGet);
        X509Certificate x509Certificate = null;
        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
                JSONObject certificateVo = JSON.parseObject(EntityUtils.toString(response.getEntity()), JSONObject.class);
                List<JSONObject> jsonObjectList = JSON.parseArray(certificateVo.getString("data"), JSONObject.class);
                for (JSONObject jsonObject : jsonObjectList) {
                    //判断证书是否再有效期内
                    if (DateUtil.compare(new Date(), jsonObject.getTimestamp("effective_time")) > 0
                            && DateUtil.compare(new Date(), jsonObject.getTimestamp("expire_time")) < 0) {
                        JSONObject encrypt_certificate = jsonObject.getJSONObject("encrypt_certificate");
                        com.wechat.pay.contrib.apache.httpclient.util.AesUtil aesUtil = new AesUtil(WechatPayUtil.apiV3Key.getBytes("utf-8"));
                        String pulicKey = aesUtil.decryptToString(encrypt_certificate.getString("associated_data").getBytes("utf-8"), encrypt_certificate.getString("nonce").getBytes("utf-8"), encrypt_certificate.getString("ciphertext"));
                        //获取平台证书
                        final CertificateFactory cf = CertificateFactory.getInstance("X509");
                        ByteArrayInputStream inputStream = new ByteArrayInputStream(pulicKey.getBytes(StandardCharsets.UTF_8));
                        x509Certificate = (X509Certificate) cf.generateCertificate(inputStream);
                    }
                }
            } else if (statusCode == 204) { //处理成功,无返回Body
                System.out.println("success");
            } else {
                System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
        }
        return x509Certificate;
    }
3.2 返回参数解析

当通知验签通过后,再进行返回参数解析,解析方式参考 AesUtil 。解析成功的返回值参考支付通知文档 。

    //解析返回值,
    public String getPayResult(Map<String, Object> body) throws Exception{
        //json 为alibaba的fastjson 
        JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(body), JSONObject.class);
        JSONObject resource = jsonObject.getJSONObject("resource");
        //WeChatPayAesUtil就是上面的AesUtil
        return WeChatPayAesUtil.decryptToString(resource.getString("associated_data").getBytes(), resource.getString("nonce").getBytes(), resource.getString("ciphertext"));

    }

4. 其他业务逻辑省略...

正文到此结束