• 微信支付API v3接口使用应用篇


    @

    前言

    最近新项目中有涉及到微信支付相关接口业务的交互,毕竟原先开发接触过支付这块,轻车熟路。打开微信支付官方文档,好家伙,微信支付API 升级至v3版本了,心中一万匹草泥马奔涌而来,根据以往对微信开发文档的认识,赶紧倒杯水,喝一喝,压压惊。
    喝完之后,开启了微信支付API v3的对接之路。

    版本

    jdk:1.8
    wechatpay-apache-httpclient:0.2.2
    

    应用

    笔者以微信小程序支付接口为例展开说明,至于小程序注册、认证、微信支付注册本文概不说明。

    基础配置

    1.申请商户API证书

    登录微信支付后台,进入账户中心,API安全设置,如下图

    申请商户证书,如下图

    点击“申请证书”按钮后,弹出生成API证书申请框,如下图

    根据提示下载证书工具,当前页面不要关闭,下载证书工具后打开,如下图

    点击“申请证书”按钮后,进入填写商户信息界面,商户信息经测试是自动填充的,如下图

    点击“下一步”,进入复制请求串界面,如下图

    将证书请求串进行复制,复制后回到上述微信支付后台申请API证书页面,将请求串进行复制,经测试自动帮你完成复制粘贴,请求串复制后点击“下一步”操作,进入复制证书串步骤,如下图

    点击“复制证书串”,将复制的证书串粘贴至证书工具中,如下图

    点击“下一步”完成商户证书的申请,如下图

    商户API证书已生成,点击查看证书文件夹,即可查看证书信息,如下图

    在微信支付商户后台可获取商户API证书序列号及证书有效期,如下图

    2.设置接口密钥

    设置API密钥,现阶段由于微信支付原先老接口并未全部升级至API v3版,涉及新老接口共存的情况,所以API密钥及APIV3密钥都需进行设置,新老接口请查阅微信支付开发文档

    API v2老版本密钥设置

    API v3密钥设置

    3.下载平台证书

    微信平台证书下载,查阅开发文档,微信已提供证书下载工具 如下图

    关注本文末尾微信公众号,回复“666”获取常用开发工具包,内含常用开发组件及微信证书下载工具,节省FQ下载时间。

    下载平台证书至本地,执行命令

    必需参数有:
    
    商户的私钥文件,即 -f (商户API证书中apiclient_key.pem文件路径)
    证书解密的密钥,即 -k (微信支付后台设置的APIv3密钥)
    商户号,即 -m (微信支付商户号,可在微信支付后台查阅)
    保存证书的路径,即 -o (微信平台证书保存路径)
    商户证书的序列号,即 -s (商户API证书,即上述第一步申请商户API证书序列号)
    非必需参数有:
    
    微信支付证书,用于验签,即 -c
    
    完整命令如下
    
    java -jar CertificateDownloader-1.1.jar -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
    

    至此基础配置参数已准备就绪

    接口实测

    在请求接口之前先了解下接口中一些参数概念,首次接触最容易搞混的就是商户API证书和平台证书,如下图

    微信支付API官方客户端

    以往老吐槽微信支付接口文档不友好,没有sdk,现在API v3版本给你提供了一个客户端,这点还是可以点赞的,对于开发人员来说,demo代码直接拿过来,更改下配置参数就可以跑通,那简直是对程序员莫大的关怀,这方面阿里相对做的比较好

    talk is cheap, show me the code

    1.客户端

    以post请求方式说明,get请求类似

    private static String basePostRequest(String requestUrl,String requestJson) {
            CloseableHttpClient httpClient = null;
            CloseableHttpResponse response = null;
            HttpEntity entity = null;
            try {
                PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
                X509Certificate wechatpayCertificate = PemUtil.loadCertificate(new ByteArrayInputStream(certificate.getBytes("utf-8")));
                ArrayList<X509Certificate> listCertificates = new ArrayList<>();
                listCertificates.add(wechatpayCertificate);
    
                httpClient = WechatPayHttpClientBuilder.create()
                        .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
                        .withWechatpay(listCertificates)
                        .build();
    
                HttpPost httpPost = new HttpPost(requestUrl);
    
                // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误
                StringEntity reqEntity = new StringEntity(requestJson, ContentType.create("application/json", "utf-8"));
                httpPost.setEntity(reqEntity);
                httpPost.addHeader("Accept", "application/json");
                response = httpClient.execute(httpPost);
                entity = response.getEntity();
                return EntityUtils.toString(entity);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 关闭流
            }
            return null;
        }
    

    方法中涉及参数说明

    参数名 说明
    privateKey 商户私钥(商户API证书apiclient_key.pem文件内容)
    certificate 平台证书(通过CertificateDownloader下载的证书文件内容)
    mchId 微信支付商户号
    mchSerialN 商户API证书序列号

    PemUtil.java类为com.wechat.pay.contrib.apache.httpclient.util.PemUtil

    请求签名及应答签名校验该客户端均已帮你处理好,心中对微信支付开发文档有了一点点好感。根据具体接口方法传入接口地址及相应接口参数JSON数据即可完成接口联调测试。

    2.支付调起参数签名

    小程序调起支付接口为例,简单说明参数签名方式,先看下文档中签名是怎么说的,如下图

     /**
         * 微信支付-前端唤起支付参数
         * prepay_id=wx201410272009395522657a690389285100
         * @param packageStr 预下单接口返回数据 预支付交易会话标识	prepay_id
         * @return
         */
        public static Map<String,Object> createPayParams(String packageStr) {
            Map<String,Object> resultMap = new HashMap<>();
            String nonceStr = StringUtil.getUUID();
            Long timestamp = System.currentTimeMillis() / 1000;
            String message = buildMessage(timestamp, nonceStr, packageStr);
            String signature = null;
            try {
                signature = sign(message.getBytes("utf-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            resultMap.put("appId", appId);
            resultMap.put("timeStamp",timestamp.toString());
            resultMap.put("nonceStr",nonceStr);
            resultMap.put("package",packageStr);
            resultMap.put("signType","RSA");
            resultMap.put("paySign",signature);
            return resultMap;
        }
        /**
         * 微信支付-前端唤起支付参数-签名
         * @param message 签名数据
         * @return
         */
        public static String sign(byte[] message) {
            try{
                Signature sign = Signature.getInstance("SHA256withRSA");
                sign.initSign(getPrivateKey(keyFilePath));
                sign.update(message);
                return Base64.getEncoder().encodeToString(sign.sign());
            } catch(Exception e) {
                e.printStackTrace();
            }
            return null;
        }
        /**
         * 微信支付-前端唤起支付参数-构建签名参数
         * @param nonceStr 签名数据
         * @return
         */
        public static String buildMessage(long timestamp, String nonceStr, String packageStr) {
            return appId + "
    "
                    + timestamp + "
    "
                    + nonceStr + "
    "
                    + packageStr + "
    ";
        }
    
    
        /**
         * 微信支付-前端唤起支付参数-获取商户私钥
         *
         * @param filename 私钥文件路径  (required)
         * @return 私钥对象
         */
        public static PrivateKey getPrivateKey(String filename) throws IOException {
    
            String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
            try {
                String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                        .replace("-----END PRIVATE KEY-----", "")
                        .replaceAll("\s+", "");
    
                KeyFactory kf = KeyFactory.getInstance("RSA");
                return kf.generatePrivate(
                        new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("当前Java环境不支持RSA", e);
            } catch (InvalidKeySpecException e) {
                throw new RuntimeException("无效的密钥格式");
            }
        }
    

    方法中涉及参数说明

    参数名 说明
    appId 小程序appid
    keyFilePath 商户API证书apiclient_key.pem路径

    3.回调通知

    回调通知涉及验签及解密

    回调数据获取

    String body = request.getReader().lines().collect(Collectors.joining());
    

    验签

    /**
         * 回调验签
         * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
         * @param wechatpaySerial 回调head头部
         * @param wechatpaySignature 回调head头部
         * @param wechatpayTimestamp 回调head头部
         * @param wechatpayNonce 回调head头部
         * @param body 请求数据
         * @return
         */
        public static boolean  responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {
            FileInputStream fileInputStream = null;
            try {
                String signatureStr = buildMessage(wechatpayTimestamp, wechatpayNonce, body);
                Signature signer = Signature.getInstance("SHA256withRSA");
    
                fileInputStream = new FileInputStream(weixin_platform_cert_path);
                X509Certificate receivedCertificate = loadCertificate(fileInputStream);
                signer.initVerify(receivedCertificate);
                signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
                return signer.verify(Base64.getDecoder().decode(wechatpaySignature));
            } catch (Exception e ) {
                e.printStackTrace();
            } finally {
                if (fileInputStream != null) {
                    try {
                        fileInputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return false;
        }
    
        /**
         * 回调验签-加载微信平台证书
         * @param inputStream
         * @return
         */
        public static X509Certificate loadCertificate(InputStream inputStream) {
            try {
                CertificateFactory cf = CertificateFactory.getInstance("X509");
                X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
                cert.checkValidity();
                return cert;
            } catch (CertificateExpiredException e) {
                throw new RuntimeException("证书已过期", e);
            } catch (CertificateNotYetValidException e) {
                throw new RuntimeException("证书尚未生效", e);
            } catch (CertificateException e) {
                throw new RuntimeException("无效的证书", e);
            }
        }
        /**
         * 回调验签-构建签名数据
         * @param 
         * @return
         */
        public static String buildMessage(String wechatpayTimestamp, String wechatpayNonce, String body) {
            return wechatpayTimestamp + "
    "
                    + wechatpayNonce + "
    "
                    + body + "
    ";
        }
    

    方法中涉及参数说明

    参数名 说明
    weixin_platform_cert_path 微信平台证书路径

    解密

    // https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml
    AesUtil wxAesUtil = new AesUtil(apiv3.getBytes());
    String jsonStr = wxAesUtil.decryptToString("associated_data".getBytes(),"nonce".getBytes(),"ciphertext");
    

    方法中涉及参数说明

    参数名 说明
    apiv3 商户API v3密钥
    associated_data 附加数据 (微信回调通知数据中resource对象)
    nonce nonce (微信回调通知数据中resource对象)
    ciphertext 数据密文 (微信回调通知数据中resource对象)
    jsonStr 对resource对象进行解密后,得到的资源对象数据

    参考资料

    微信支付开发文档

    平台证书下载工具

    微信支付API v3文档介绍

    微信支付API客户端

    在这里插入图片描述

  • 相关阅读:
    Effective C++ 读书笔记之Part1.Accustoming Yourself to C++
    Effective C++ 读书笔记之Part4.Design and Declarations
    Effective C++ 读书笔记之Part5.Implementations
    给程序猿三条身体保健的建议
    亮剑.NET的系列文章之.NET详解(开端)
    [转]英特尔工具使在多个处理器上实现线程化更加轻松
    [转]TrackBack Technical Specification
    Chapter 03 Writing Executable Statements 01
    Chapter 09Manipulating Data 01
    Chapter 00 Overview to PL/SQL
  • 原文地址:https://www.cnblogs.com/chinaWu/p/14643814.html
Copyright © 2020-2023  润新知