微信商户代金券发送V3版(java语言版)

微信商户代金券发送V3版(Java语言)
想了很久还是想整理一下微信商户代金券V3版的发送,自己踩过的接口,之前公司用的一直是v2版,个人觉得v2配置的简单一点,v3就是配置稍微复杂一点,废话不多说
1.思路:新版的商户代金券的发送接口强调更加安全,但是返回数据就特别简单
<1.>请求微信官方,获取到微信公钥

/**
 * 获取公钥的方式,并把获取的公钥保存在D:\key
 * D:\key(自己设置的地址)
 */
@Slf4j
public class PublicKeyUtil {

    public static void main(String[] args) throws Exception {
        List<X509Certificate> certificateList = getCertByAPI("商户号", "https://api.mch.weixin.qq.com/v3/certificates", 2, null, "证书序列号", "D:\\wxpert\\apiclient_key.pem");
        log.info("====certificateList====",certificateList);
    }

    public static List<X509Certificate> getCertByAPI(String merchantId, String url, int timeout, String body, String certSerialNo, String keyPath) throws Exception {
        String result = "";
        //创建http请求
        log.info("====获取公钥的传参====,merchantId={},url={},timeout={},body={},certSerialNo={},keyPath={}",merchantId,url,timeout,body,certSerialNo,keyPath);
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Content-Type", "application/json");
        httpGet.addHeader("Accept", "application/json");

        //设置认证信息
        httpGet.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + getToken("GET", url, null, merchantId, certSerialNo, keyPath));

        //设置请求器配置:如超时限制等
        RequestConfig config = RequestConfig.custom().setSocketTimeout(timeout * 1000).setConnectTimeout(timeout * 1000).build();
        httpGet.setConfig(config);
        List<X509Certificate> x509Certs = new ArrayList<X509Certificate>();
        try {
            CloseableHttpClient httpClient = HttpClients.createDefault();
            CloseableHttpResponse response = httpClient.execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();
            HttpEntity httpEntity = response.getEntity();
            result = EntityUtils.toString(httpEntity, "UTF-8");
            if (statusCode == 200) {
                log.info("下载平台证书返回结果:" + result);
                List<CertificateItem> certList = new ArrayList<CertificateItem>();
                JSONObject json = JSONObject.parseObject(result);
                log.info("查询结果json字符串转证书List:" + json.get("data"));
                JSONArray jsonArray = (JSONArray) json.get("data");
                for (int i = 0; i < jsonArray.size(); i++) {
                    CertificateItem certificateItem = new CertificateItem();
                    EncryptedCertificateItem encryptCertificate = new EncryptedCertificateItem();
                    JSONObject bo = JSONObject.parseObject(jsonArray.get(i).toString());
                    certificateItem.setSerial_no(bo.get("serial_no").toString());
                    certificateItem.setEffective_time(bo.get("effective_time").toString());
                    certificateItem.setExpire_time(bo.get("expire_time").toString());
                    JSONObject encryptBo = JSONObject.parseObject(bo.get("encrypt_certificate").toString());
                    encryptCertificate.setAlgorithm(encryptBo.get("algorithm").toString());
                    encryptCertificate.setNonce(encryptBo.get("nonce").toString());
                    encryptCertificate.setAssociated_data(encryptBo.get("associated_data").toString());
                    encryptCertificate.setCiphertext(encryptBo.get("ciphertext").toString());
                    certificateItem.setEncrypt_certificate(encryptCertificate);
                    certList.add(certificateItem);
                }
                log.info("证书List:" + certList);

                List<PlainCertificateItem> plainList = decrypt(certList, response);
                if (CollectionUtils.isNotEmpty(plainList)) {
                    log.info("平台证书开始保存");
                    x509Certs = saveCertificate(plainList);
                }
            }
            response.close();
            httpClient.close(); //throw
            return x509Certs;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("下载平台证书返回结果:" + e);
        }
        return x509Certs;
    }

    private static List<PlainCertificateItem> decrypt(List<CertificateItem> certList, CloseableHttpResponse response) throws GeneralSecurityException, IOException {
        List<PlainCertificateItem> plainCertificateList = new ArrayList<PlainCertificateItem>();
//      XXX为自己的apiv3秘钥
        WeiXinAesUtil weiXinAesUtil = new WeiXinAesUtil(("XXX").getBytes(StandardCharsets.UTF_8));
        for (CertificateItem item : certList) {
            PlainCertificateItem bo = new PlainCertificateItem();
            bo.setSerialNo(item.getSerial_no());
            bo.setEffectiveTime(item.getEffective_time());
            bo.setExpireTime(item.getExpire_time());
            log.info("平台证书密文解密");
            bo.setPlainCertificate(weiXinAesUtil.decryptToString(item.getEncrypt_certificate().getAssociated_data().getBytes(StandardCharsets.UTF_8),
                    item.getEncrypt_certificate().getNonce().getBytes(StandardCharsets.UTF_8), item.getEncrypt_certificate().getCiphertext()));
            log.info("平台证书公钥明文:" + bo.getPlainCertificate());
            plainCertificateList.add(bo);
        }
        return plainCertificateList;
    }

    //证书保存地址
    private static List<X509Certificate> saveCertificate(List<PlainCertificateItem> cert) throws IOException {
        List<X509Certificate> x509Certs = new ArrayList<X509Certificate>();
        // 公钥保存在D:\key文件夹下名字为publicKey.pem
        File file = new File("D:\\key");
        file.mkdirs();
        for (PlainCertificateItem item : cert) {
            ByteArrayInputStream inputStream = new ByteArrayInputStream(item.getPlainCertificate().getBytes(StandardCharsets.UTF_8));
            X509Certificate x509Cert = PemUtil.loadCertificate(inputStream);
            x509Certs.add(x509Cert);
            String outputAbsoluteFilename = file.getAbsolutePath() + File.separator + "publicKey.pem";
            try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputAbsoluteFilename), StandardCharsets.UTF_8))) {
                writer.write(item.getPlainCertificate());
            }
            log.info("输出证书文件目录:" + outputAbsoluteFilename);
        }
        return x509Certs;
    }

}

请求到微信,返回的公钥,我们需要保存,我这里写的是工具类,正式使用,需要上传到服务器
<2.>验证微信的公钥

/**
 * 微信验签工具类
 */
@Slf4j
public class CheckSignUtil {
    private static String CHARSET_ENCODING = "UTF-8";
    private static String ALGORITHM = "SHA256withRSA";

    public static void main(String[] args) throws Exception {
        getCheckSign("商户号", "https://api.mch.weixin.qq.com/v3/certificates", 2, null, "商户证书序列号", "D:\\wxpert\\apiclient_key.pem");
    }

    public static Boolean getCheckSign(String merchantId, String url, int timeout, String body, String certSerialNo, String keyPath) throws UnsupportedEncodingException, Exception {
        String result = "";
        boolean verify=false;
        //创建http请求
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Content-Type", "application/json");
        httpGet.addHeader("Accept", "application/json");

        //设置认证信息
        httpGet.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + getToken("GET", url, null, merchantId, certSerialNo, keyPath));

        //设置请求器配置:如超时限制等
        RequestConfig config = RequestConfig.custom().setSocketTimeout(timeout * 1000).setConnectTimeout(timeout * 1000).build();
        httpGet.setConfig(config);
        try {
            CloseableHttpClient httpClient = HttpClients.createDefault();
            CloseableHttpResponse response = httpClient.execute(httpGet);
            HttpEntity httpEntity = response.getEntity();
            result = EntityUtils.toString(httpEntity, "UTF-8");
            Header[] allHeaders = response.getAllHeaders();
            Map<String, String> headers = new HashMap<String, String>();
            for (int i = 0; i < allHeaders.length; i++) {
                String key = allHeaders[i].getName();
                String value = allHeaders[i].getValue();
                headers.put(key, value);
            }
            String Nonce = headers.get("Wechatpay-Nonce");
            String Signature = headers.get("Wechatpay-Signature");
            String Timestamp = headers.get("Wechatpay-Timestamp");
            String srcData = Timestamp + "\n"
                    + Nonce + "\n"
                    + result + "\n";
            // publicKeyPath 为自己公钥保存的地址如:D:\key\publicKey.pem
            String publicKeyPath = "D:\\key\\publicKey.pem";
             verify = verify(srcData, Signature, publicKeyPath);
            log.info("验签结果 = " + verify);
        } catch (Exception e) {
            log.error("======验签getCheckSign出现异常=======",e);
        }
        return verify;
    }

    /**
     * 验签
     *
     * @param srcData
     * @param signedData
     * @param publicKeyPath
     * @return
     */
    public static boolean verify(String srcData, String signedData, String publicKeyPath) {
        if (srcData == null || signedData == null || publicKeyPath == null) {
            return false;
        }
        try {
            PublicKey publicKey = readPublic(publicKeyPath);
            Signature sign = Signature.getInstance(ALGORITHM);
            sign.initVerify(publicKey);
            sign.update(srcData.getBytes(CHARSET_ENCODING));
            return sign.verify(Base64.getDecoder().decode(signedData));
        } catch (Exception e) {
           log.info("验签verify出现异常",e);
        }
        return false;
    }


    /**
     * 读取公钥
     *
     * @param publicKeyPath
     * @return
     */
    private static PublicKey readPublic(String publicKeyPath) {
        if (publicKeyPath == null) {
            return null;
        }
        PublicKey pk = null;
        FileInputStream bais = null;
        try {
            CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509");
            bais = new FileInputStream(publicKeyPath);
            X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(bais);
            pk = cert.getPublicKey();
        } catch (CertificateException e) {
            log.error("读取公钥readPublic出现CertificateException异常",e);
        } catch (FileNotFoundException e) {
            log.error("读取公钥readPublic出现FileNotFoundException异常",e);
        } finally {
            if (bais != null) {
                try {
                    bais.close();
                } catch (IOException e) {
                    log.error("读取公钥readPublic出现IOException异常",e);
                }
            }
        }
        return pk;
    }
}
<3.>获取签名
@Slf4j
public class WeiXinUtil {
    public static String getToken(String method, String url, String body, String merchantId, String certSerialNo, String keyPath) throws Exception {
        HttpUrl httpurl = HttpUrl.parse(url);
        String nonceStr = StringUtil.getRandomKey(32);
        long timestamp = System.currentTimeMillis() / 1000;
        if (StringUtils.isEmpty(body)) {
            body = "";
        }
        String message = buildMessage(method, httpurl, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"), keyPath);
        return "mchid=\"" + merchantId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + certSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    public static String sign(byte[] message, String keyPath) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(keyPath));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    public static 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";
    }

    /**
     * 获取私钥。
     *
     * @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("无效的密钥格式");
        }
    }

    //发送代金券v3版
    public static String sendCoupon(String mchid, String url, String stockId, String appId, String openid) throws Exception {
        String replaceUrl = url.replace("{}", openid);
        String result = "";
        String key = StringUtil.getRandomKey(6);
        String s = DateUtil.currentDate();
        String partnerTradeNo = mchid + s + key;
        log.info("商户单据号:" + partnerTradeNo);
        //商户证书序列号
        String WX_SERIAL_NO = "467AD5AC9B1AD5784C60BB0FBB5E4F8DFD59CCA9";
        // 商户证书签名私钥路径(最好不要有中文)
        String keyPath = "D:\\wxpert\\apiclient_key.pem";
        HttpClient client = HttpClients.createDefault();
        Map<String, String> paramMap = new HashMap<>();
//        paramMap.put("stock_id", "10190992");
//        paramMap.put("out_request_no", partnerTradeNo);
//        paramMap.put("appid", "wxb9472f16ffb7c61a");
//        paramMap.put("stock_creator_mchid", mchid);
        paramMap.put("stock_id", stockId);
        paramMap.put("out_request_no", partnerTradeNo);
        paramMap.put("appid", appId);
        paramMap.put("stock_creator_mchid", mchid);
        List<X509Certificate> certificateList = PublicKeyUtil.getCertByAPI(mchid, WxApiEnum.GET_CERT.getUrl(), 2, null, WX_SERIAL_NO, keyPath);
        log.info("获取微信平台的证书结果certificateList={}", certificateList);
        Boolean checkSign = CheckSignUtil.getCheckSign(mchid, WxApiEnum.GET_CERT.getUrl(), 2, null, WX_SERIAL_NO, keyPath);
        log.info("验签结果checkSign={}", checkSign);
        HttpPost httpPost = new HttpPost(replaceUrl);
        httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " "
                + getToken("POST", replaceUrl, JSONObject.toJSONString(paramMap), mchid, WX_SERIAL_NO, keyPath));
        httpPost.setEntity(new StringEntity(JSONObject.toJSONString(paramMap), "UTF-8"));
        try {
            HttpEntity entity = client.execute(httpPost).getEntity();
            if (entity != null) {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
                StringBuffer resultBody = new StringBuffer();
                String text;
                while ((text = bufferedReader.readLine()) != null) {
                    resultBody.append(text);

                }
                result = resultBody.toString();
            }
            EntityUtils.consume(entity);
            log.info("====返回的结果===", result);
        } catch (Exception e) {
            log.error("发送代金券v3版异常", e);
        }
        return result;
    }

}

获取签名之前你需要初始化你的商户私钥,这个官方给的也有的,我写在WeiXinUtil里了
下面是我用的工具类,还有一些用的类

@Slf4j
public class PemUtil {
    public static X509Certificate loadCertificate(ByteArrayInputStream inputStream) throws IOException {
        X509Certificate cert = null;
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            cert = (X509Certificate) cf.generateCertificate(inputStream);
        } catch (CertificateExpiredException e) {
            throw new RuntimeException("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new RuntimeException("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new RuntimeException("无效的证书文件", e);
        } finally {
            inputStream.close();
        }
        return cert;
    }
}
 //AES解密工具类
public class WeiXinAesUtil {
 
    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;
 
    public WeiXinAesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }
 
    public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
            throws GeneralSecurityException, IOException {
        try {
//            Security.setProperty("crypto.policy", "unlimited");
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
 
            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
 
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);
            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

//几个用到的类


//平台证书item
@Data
public class CertificateItem {

    //加密的平台证书序列号
    private String serial_no;

    //加密的平台证书序列号
    private String effective_time;

    //证书弃用时间
    private String expire_time;

    //证书加密信息
    private EncryptedCertificateItem encrypt_certificate;
}
@Data
//证书加密信息
public class EncryptedCertificateItem {
    //加密的平台证书序列号
    private String algorithm;

    //加密的平台证书序列号
    private String nonce;

    //证书弃用时间
    private String associated_data;

    //证书弃用时间
    private String ciphertext;
}

//证书明文item
@Data
public class PlainCertificateItem {

    private String serialNo;

    private String effectiveTime;

    private String expireTime;

    private String plainCertificate;

}

这是我参考的博客:添加链接描述

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值