解决-微信支付API V3版-所有问题

本文包括P12证书文件反解私钥、公钥、微信支付APIV3版的获取平台证书接口,加解密方法, jsapi支付接口等。

微信支付更新了接口规则,将以往的xml数据格式改为json,增加了一系列签名验签证书等,但是官方文档并不完善且问题较杂较多,罗列一下自己的解决办法。

这里是微信支付的开发者文档:

微信支付-开发者文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml

 对于微信接口的流程图、时序图这里就不赘述了,相信在使用接口的过程中,按照目前文档的粒度,是必然会出现问题的,这里直接上解决问题的办法。

一、前期准备

登录商户微信号,在API安全中,你可以获取到如下:

商户号、appid、证书、API密钥、APIV3密钥

※若非新申请证书,可能会出现只有p12文件,无原始证书,这里需要根据p12文件反解出之前的证书,但是需要证书的口令,没有口令则没有任何办法,方法如下:

1、windows下可以直接双击p12证书文件,在导入过程会提醒输入密码,此时方便你测试你的口令。

2、提取

打开cmd,利用OpenSSL命令提取RSA原始密钥对。

openssl pkcs12 -in 你的证书.p12 -nocerts -nodes -out rsa_origin.key

密钥内容一般由-----BEGIN PRIVATE KEY-----开始,以-----END PRIVATE KEY-----结束,类似于:

此时为原始密钥,还不能直接使用,一般来说,P12证书->口令解密->原始密钥->公钥、私钥。

提取私钥:

openssl rsa -in rsa_origin.key -out rsa_pkcs1.pem

 此时获取的密钥为PKCS#1格式,PHP可直接使用,我们java需要再次转换:

openssl pkcs8 -topk8 -inform PEM -in rsa_pkcs1.pem -outform PEM -out rsa_private_pkcs8.pem -nocrypt

提取公钥

openssl rsa -in rsa_origin.key -pubout -out rsa_public_key.pem

至此,通过P12证书,可以获取到三个文件,分别是原始密钥rsa_origin.key,1格式私钥rsa_pkcs1.pem,8格式私钥rsa_private_pkcs8.pem,和公钥rsa_public_key.pem。

到这里,微信支付的商户证书环节就够了,API密钥和APIV3密钥是字符串,也是设置的时候才能保存,微信不提供获取。

现在,你拥有:API密钥、APIV3密钥、私钥文件rsa_private_pkcs8.pem(文件打开也能拿到私钥内容)、商户ID、APPID、APPsecret、证书序列号。

二、正题

在官方提供的SDK中,提供了一个封装的httpclient,使用这个client进行接口的调用。

在你以为上面的准备已经足够了时候,微信支付一定会给你一个惊喜。

熟读简单支付JSAPI下单接口文档后,发现openid这个参数也需要我们自己获取,这个接口并没有验签签名,相对来说简单很多。

直接贴代码

        log.info("getUserInfo user is :{} ,  code:{}", u, code);
        String url = "https://api.weixin.qq.com/sns/jscode2session";
        url += "?appid=appid";//自己的appid
        url += "&secret=secret";//自己的appSecret
        url += "&js_code=" + code;
        url += "&grant_type=authorization_code";
        String res = null;
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        // DefaultHttpClient();
        HttpGet httpget = new HttpGet(url);    //GET方式
        CloseableHttpResponse response = null;
        // 配置信息
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000).setConnectionRequestTimeout(5000).setSocketTimeout(5000).setRedirectsEnabled(false).build();
        httpget.setConfig(requestConfig);
        response = httpClient.execute(httpget);
        HttpEntity responseEntity = response.getEntity();
        log.info("响应状态为:{}", response.getStatusLine());
        if (responseEntity != null) {
            res = EntityUtils.toString(responseEntity);
            log.info("响应内容长度为:{}", responseEntity.getContentLength());
            log.info("响应内容为:{}", res);
        }
        // 释放资源
        if (httpClient != null) {
            httpClient.close();
        }
        if (response != null) {
            response.close();
        }
        JSONObject jo = JSON.parseObject(res);
        String errcode = jo.getString("errcode");
        if (!StringUtils.isEmpty(errcode) && !errcode.equals("0")) {
            return Result.error(res);
        }
        String openid = jo.getString("openid");

这就获取到了openId,珍惜为数不多的简单接口。

再次熟读JSAPI下单接口接口,发现参数我们都有了,下面调用即可,上面说了,官方已经提供了SDK,但是在我们悉心挖掘后发现,SDK只提供了一个client,所以我们使用这个client进行接口的调用即可,嗯,听起来简单的很。

这里官方SDK:GitHub - wechatpay-apiv3/wechatpay-apache-httpclient: 微信支付 APIv3 Apache HttpClient装饰器(decorator)

这里是依赖:

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

这里是SDK的构建client示例

明确概念很重要,这里的商户号、证书序列号都可以通过商户微信号在API安全里面获取到,这里的商户API私钥,就是刚才我们反解出来的 私钥文件rsa_private_pkcs8.pem(文件打开也能拿到私钥内容)

这里的 微信支付平台证书 ,没错,你现在没有,这个不是上面的任何一个证书,所以接下来,我们要继续获取证书。

参考官方文档:微信支付-开发者文档https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_1.shtml

 这里就是获取证书的接口,接口很简单,不需要参数,但是需要header中放入签名Authorization,所以这个接口的坑大部分都是构建Authorization和解密返回结果,这里容我不想废话,直接上代码

获取证书列表 

    private JSONObject getCert() throws Exception {

        //header的值
        Map headers = new HashMap(1);
        HttpUrl httpurl = HttpUrl.parse("https://api.mch.weixin.qq.com/v3/certificates");
        String t = getToken("GET", httpurl, "");
        headers.put("Authorization", "WECHATPAY2-SHA256-RSA2048 " + t);
        //加入自己的url
        StringBuffer url = new StringBuffer("https://api.mch.weixin.qq.com/v3/certificates");
        //参数封组装
        RestTemplate restTemplate = new RestTemplate();
        //组装请求头
        HttpHeaders requestHeaders = this.buildHeaders(headers);
        HttpEntity<String> requestEntity = new HttpEntity(requestHeaders);
        ResponseEntity<String> response = restTemplate.exchange(url.toString(),
                HttpMethod.GET, requestEntity, String.class, new Object[0]);
        JSONObject result = JSONObject.parseObject(response.getBody(), JSONObject.class);

        return result;
    }

构建签名串

    String getToken(String method, HttpUrl url, String body) throws Exception {
        String nonceStr = "nonceStr";
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"));
        return "mchid=\"" + "mchid" + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + "serial_no" + "\","
                + "signature=\"" + signature + "\"";
    }

构建签名前数据

    String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }

        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

构建header 

    protected HttpHeaders buildHeaders(Map<String, String> headers) {
        HttpHeaders reqHeaders = new HttpHeaders();
        if (null != headers) {
            for (String key : headers.keySet()) {
                if (null == headers.get(key)) {
                    reqHeaders.add(key, "");
                }
                reqHeaders.add(key, headers.get(key));
            }
        }

        return reqHeaders;
    }

签名

    String sign(byte[] message) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");


        String s = "-----BEGIN PRIVATE KEY-----" +
                "-----END PRIVATE KEY-----";
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(s);

        sign.initSign(merchantPrivateKey);
        sign.update(message);

        return java.util.Base64.getEncoder().encodeToString(sign.sign());
    }

 注意,PemUtil.loadPricateKey函数,有两个重载方法,string入参的时候,入参为密钥里面的内容,InputStream为入参时,直接getClass().getResourceAsStream("/pay/path")也可以。

这个密钥,就是上面,私钥文件rsa_private_pkcs8.pem(文件打开也能拿到私钥内容)

获取参数

    private String dec(JSONObject res) {
        log.info("===================:{}", res);
        CertVo sign = JSON.parseObject(res.toJSONString(), new TypeReference<CertVo>() {});
        List<CertVo.DataBean> data = sign.getData();
        String s = "";
        for (int i = 0; i < data.size(); i++) {
            CertVo.DataBean dataBean = data.get(i);
            CertVo.DataBean.EncryptCertificateBean encrypt_certificate = dataBean.getEncrypt_certificate();
            String associated_data = encrypt_certificate.getAssociated_data();
            String ciphertext = encrypt_certificate.getCiphertext();
            String nonce = encrypt_certificate.getNonce();
            String apiv3 = "apiv3";
            s = decryptResponseBody1(apiv3, associated_data, nonce, ciphertext);
        }

        return s;
    }

 解密

    public String decryptResponseBody1(String apiV3Key,String associatedData, String nonce, String ciphertext) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
            byte[] bytes;
            try {
                bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
            } catch (GeneralSecurityException e) {
                throw new IllegalArgumentException(e);
            }
            return new String(bytes, StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }

实体类 

@Data
public class CertVo implements Serializable {


    private List<DataBean> data;

    @Data
    public static class DataBean implements Serializable {
        /**
         * effective_time : 2021-03-11T13:55:55+08:00
         * encrypt_certificate : {"associated_data":"certificate","ciphertext":"DG","nonce":"nonce","algorithm":"AEAD_AES_256_GCM"}
         * expire_time : 2026-03-10T13:55:55+08:00
         * serial_no : serial_no 
         */

        private String effective_time;
        private EncryptCertificateBean encrypt_certificate;
        private String expire_time;
        private String serial_no;

        @Data
        public static class EncryptCertificateBean implements Serializable {
            /**
             * associated_data : certificate
             * ciphertext : DG
             * nonce : nonce 
             * algorithm : algorithm 
             */

            private String associated_data;
            private String ciphertext;
            private String nonce;
            private String algorithm;
        }
    }
}

由于参数等原因,上述代码是简版,调通借口后细节配置等还需要完善。

至此,你获取到了一个平台证书的解密后的字符串类型,即dec函数的返回值。

获取证书

        CertificateFactory cf = CertificateFactory.getInstance("X509");

        ByteArrayInputStream inputStream = new ByteArrayInputStream(dec.getBytes(StandardCharsets.UTF_8));
        Certificate certificate = null;
        try {
            certificate = cf.generateCertificate(inputStream);
        } catch (CertificateException e) {
            e.printStackTrace();
        }

OK,接下来构建client,直接上

构建client

    public HttpClient initWChant() throws Exception {

        String s = "-----BEGIN PRIVATE KEY-----" +

                "-----END PRIVATE KEY-----";
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(s);

        JSONObject cert = getCert();
        String dec = dec(cert);

        CertificateFactory cf = CertificateFactory.getInstance("X509");

        ByteArrayInputStream inputStream = new ByteArrayInputStream(dec.getBytes(StandardCharsets.UTF_8));
        Certificate certificate = null;
        try {
            certificate = cf.generateCertificate(inputStream);
        } catch (CertificateException e) {
            e.printStackTrace();
        }

        List<X509Certificate> a = new ArrayList<>();
        a.add((X509Certificate) certificate);


        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant("merchantId","merchantSerialNumber", merchantPrivateKey)
                .withWechatPay(a);

        return builder.build();
    }

注意,PemUtil.loadPricateKey函数,有两个重载方法,string入参的时候,入参为密钥里面的内容,InputStream为入参时,直接getClass().getResourceAsStream("/pay/path")也可以。

至此,就拿到了client,官方也说会不定时更新他们的支付平台证书,这点在实战是需要考虑在内,进行设计。

在此过程,会出现n多bug,其中最难解决的是使用jdk8 151以下版本时,解密时会出现

java.security.InvalidKeyException: Illegal key size

大部分同行会告诉你换文件,随便能搜到,就不贴了,这里我推荐直接更换jdk,换个高版本的jdk会更快解决这个问题。各种方法各有利弊。

接下来就是获取prepay_id的,直接上

 HttpPost httpPost = new HttpPost(VX_APP_PAY_URL);
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode rootNode = objectMapper.createObjectNode();
        //商户id
        rootNode.put("mchid", "mchid")
                //小程序id
                .put("appid", "mchid")
                //描述
                .put("description", "description")
                //微信通知回调地址
                .put("notify_url", "")
                //商户订单id
                .put("out_trade_no", tOrder.getOrderNum());
        BigDecimal bigDecimal = payVo.getMoney().movePointRight(2);
        rootNode.putObject("amount").put("total", bigDecimal.intValue());
        rootNode.putObject("payer").put("openid", openId);
        log.info("===============:{}",rootNode);
        objectMapper.writeValue(bos, rootNode);
        httpPost.setEntity(new StringEntity(bos.toString("UTF-8")));
        HttpClient httpClient = initWChant();
        HttpResponse response = httpClient.execute(httpPost);
        String bodyAsString = EntityUtils.toString(response.getEntity());

你可以使用各种json工具,取出你要的prepay_id。

本以为到此结束,但是发现jsapi调起支付接口中,也涉及到了签名作为参数,不得不感叹,微信你是真滴安全,前端表示可不可以后端一起返回,后端表示可以,直接上:

        long timeStamp = System.currentTimeMillis() / 1000;
        String nonceStr = WXPayUtil.generateNonceStr();
        String dataStr = "appId" + "\n" + timeStamp + "\n" + nonceStr + "\n" + "prepay_id=" + prepay_id.toString() + "\n";
        //要加密的数据
        System.out.println("要加密的数据:" + dataStr);
        String signature = sign(dataStr.getBytes("utf-8"));
        Map<String, String> payMap = new HashMap<>();
        payMap.put("appId", "wx7c97a277f6ba30b4");
        payMap.put("timeStamp", timeStamp + "");
        payMap.put("nonceStr", nonceStr);
        payMap.put("signType", "RSA");
        payMap.put("package", "prepay_id=" + prepay_id);
        payMap.put("paySign", signature);

对,没错,加密方法就是上面的加密方法,其实很简单,所以才会表示可以。

到这里,对接微信支付PAIV3版接口的,支付下单接口就完成了,后面的回调验签解密,直接调用上面的方法就可以,结合业务逻辑操作即可。

不得不说,在微信开放社区那么多人提问的前提下,还是需要一步一步摸索,才搞清楚整体流程需要的东西,搞通这一个下单支付接口还花费了两天时间,实在是感叹,微信支付APIV3,你是真滴安全。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值