当前位置: 首页 > 工具软件 > Dos.WeChat > 使用案例 >

微信支付API V3图片上传 实现

闻人杰
2023-12-01

微信支付文档之大坑

最近有个项目要开发微信支付之服务商模式,作为服务商提供给商户入驻则需要接入微信特邀商户入驻功能。

这里面恶心的不止是接微信特邀商户入驻申请API(将近100个字段),最最恶心的是有个微信支付业务指定商户需要使用图片或视频的API,跟着API文档走你会发现一直卡在两个请求错误之间。

错误1:图片sha256值计算有误,请检查算法,重新计算后提交
错误2:签名错误,验签失败

实际上是跟着API文档和SDK接入的,这SDK和API有大坑,建议不要使用!!!

在网上找了好久也没有找到解决方法,搞了好几天最后莫名其妙的解决了,下面直接贴上代码,复制就可使用。

微信支付业务指定商户需要使用图片上传 API

API文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter2_1_1.shtml

package com.keelea.pay.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONObject;
import com.keelea.common.core.utils.file.MultipartFileToFileUtils;
import com.keelea.pay.utils.CertHttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Service
public class wxPayTest {

	 /**
	  * @param multipartFile 图片文件流
	  */
	public void wxPayImageUpload(File file) {
        String serialNo ="你的微信服务商证书序列号";
        String merchantId = "你的微信服务商商户号";
        //取  -----BEGIN PRIVATE KEY-----   和  -----END PRIVATE KEY-----  之间的内容
        String privateKeyPem = "你的微信服务商证书私钥内容";
        DataOutputStream dos = null;
        InputStream is = null;
        try {
            // 换行符
            //不能使用System.lineSeparator(),linux环境微信会报验签失败
            String lineSeparator = "\r\n";
            String boundary = "boundary";
            //必须为--
            String beforeBoundary = "--";
            //时间戳
            String timestamp = Long.toString(System.currentTimeMillis() / 1000);
            //随机数
            String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
            //文件名
            String filename = file.getName();
            is = new FileInputStream(file);
            //文件sha256值
            String fileSha256 = DigestUtils.sha256Hex(is);
            is.close();
            //拼签名串
            StringBuilder sb = new StringBuilder();
            sb.append("POST").append("\n");
            sb.append("/v3/merchant/media/upload").append("\n");
            sb.append(timestamp).append("\n");
            sb.append(nonceStr).append("\n");
            sb.append("{\"filename\":\"").append(filename).append("\",\"sha256\":\"").append(fileSha256).append("\"}").append("\n");
            log.info("签名的串:" + sb.toString());
            byte[] bytes = sb.toString().getBytes("utf-8");
            log.info("签名的串的字节长度:{}", bytes.length);
            //计算签名
            String sign = CertHttpUtil.signRSA(sb.toString(), privateKeyPem);
            log.info("签名sign值:" + sign);
            //拼装http头的Authorization内容
            String authorization = "WECHATPAY2-SHA256-RSA2048" + " mchid=\"" + merchantId
                    + "\",nonce_str=\"" + nonceStr
                    + "\",signature=\"" + sign
                    + "\",timestamp=\"" + timestamp
                    + "\",serial_no=\"" + serialNo + "\"";
            log.info("authorization值:" + authorization);
            //接口URL
            URL url = new URL("https://api.mch.weixin.qq.com/v3/merchant/media/upload");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 设置为POST
            conn.setRequestMethod("POST");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求头参数
            conn.setRequestProperty("Charsert", "UTF-8");
            conn.setRequestProperty("Accept", "application/json");
            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            conn.setRequestProperty("Authorization", authorization);
            dos = new DataOutputStream(conn.getOutputStream());
            //拼装请求内容第一部分
            String metaPart = beforeBoundary + boundary + lineSeparator +
                    "Content-Disposition: form-data; name=\"meta\";" + lineSeparator +
                    "Content-Type: application/json" + lineSeparator + lineSeparator +
                    "{\"filename\":\"" + filename + "\",\"sha256\":\"" + fileSha256 + "\"}" + lineSeparator;
            dos.writeBytes(metaPart);
            dos.flush();
            //拼装请求内容第二部分
            String filePart = beforeBoundary + boundary + lineSeparator +
                    "Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\";" + lineSeparator +
                    "Content-Type: image/jpeg" + lineSeparator + lineSeparator;
            dos.writeBytes(filePart);
            dos.flush();
            //文件二进制内容
            byte[] buffer = new byte[1024];
            int len;
            is = new FileInputStream(file);
            while ((len = is.read(buffer)) != -1) {
                dos.write(buffer, 0, len);
                //不需要写完整个文件+尾行后再flush
                dos.flush();
            }
            is.close();
            //拼装请求内容结尾
            String endLine = lineSeparator
                    + beforeBoundary
                    + boundary
                    //必须,标识请求体结束
                    + "--"
                    + lineSeparator;
            dos.writeBytes(endLine);
            dos.flush();
            dos.close();
            FileUtil.del(file);
            //接收返回
            //打印返回头信息
            StringBuilder respHeaders = new StringBuilder();
            Map<String, List<String>> responseHeader = conn.getHeaderFields();
            for (Map.Entry<String, List<String>> entry : responseHeader.entrySet()) {
                respHeaders.append(lineSeparator).append(entry.getKey()).append(":").append(entry.getValue());
            }
            log.info("微信应答的头信息:{}", respHeaders);
            //打印返回内容
            int responseCode = conn.getResponseCode();
            //应答报文主体
            String rescontent;
            if ((responseCode + "").equals("200")) {
                rescontent = new String(MultipartFileToFileUtils.toByteArray(conn.getInputStream()), "utf-8");
                log.info("微信支付业务指定商户图片上传成功,微信应答:" + rescontent);
                //转成json
                JSONObject jsonObject = new JSONObject(rescontent);
                log.info("微信支付业务指定商户图片上传成功,图片ID:" + jsonObject.getStr("media_id"));
            }
            rescontent = new String(MultipartFileToFileUtils.toByteArray(conn.getErrorStream()), "utf-8");
            log.info("微信支付业务指定商户图片上传失败,微信应答:" + rescontent);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (dos != null) {
                    dos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

工具类

public class CertHttpUtil {
    private static int socketTimeout = 10000;// 连接超时时间,默认10秒
    private static int connectTimeout = 30000;// 传输超时时间,默认30秒
    private static RequestConfig requestConfig;// 请求器的配置
    private static CloseableHttpClient httpClient;// HTTP请求器

    /**
     * 通过Https往API post xml数据
     *
     * @param url      API地址
     * @param xmlObj   要提交的XML数据对象
     * @param mchId    商户ID
     * @param certPath 证书位置
     * @return
     */
    public static String postData(String url, String xmlObj, String mchId, String certPath) {
        // 加载证书
        try {
            initCert(mchId, certPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
        String result = null;
        HttpPost httpPost = new HttpPost(url);
        // 得指明使用UTF-8编码,否则到API服务器XML的中文不能被成功识别
        StringEntity postEntity = new StringEntity(xmlObj, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.setEntity(postEntity);
        // 根据默认超时限制初始化requestConfig
        requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
        // 设置请求器的配置
        httpPost.setConfig(requestConfig);
        try {
            HttpResponse response = null;
            try {
                response = httpClient.execute(httpPost);
            } catch (IOException e) {
                e.printStackTrace();
            }
            HttpEntity entity = response.getEntity();
            try {
                result = EntityUtils.toString(entity, "UTF-8");
            } catch (IOException e) {
                e.printStackTrace();
            }
        } finally {
            httpPost.abort();
        }
        return result;
    }

    /**
     * 加载证书
     *
     * @param mchId    商户ID,默认为证书密码
     * @param certPath 证书位置
     */
    private static void initCert(String mchId, String certPath) throws Exception {
        // 指定读取证书格式为PKCS12
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        // 读取本地存放的PKCS12证书文件
        ClassPathResource classPathResource = new ClassPathResource(certPath);
        //获取文件流
        InputStream instream = classPathResource.getInputStream();

        try {
            // 指定PKCS12的密码(商户ID)
            keyStore.load(instream, mchId.toCharArray());
        } finally {
            instream.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
        SSLConnectionSocketFactory sslsf =
                new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null,
                        SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
        httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
    }

    /**
     * 获取证书
     *
     * @param inputStream 证书文件
     * @return {@link X509Certificate} 获取证书
     */
    public static X509Certificate getCertificate(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 keyPath 商户私钥证书路径
     * @return {@link PrivateKey} 商户私钥
     * @throws Exception 异常信息
     */
    public static PrivateKey getPrivateKey(String keyPath) throws Exception {
        String originalKey = FileUtil.readUtf8String(keyPath);
        String privateKey = originalKey
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");

        return RSAUtil.loadPrivateKey(privateKey);
    }

    /**
     * 获取商户私钥
     *
     * @param keyPath 商户私钥证书路径
     * @return {@link PrivateKey} 商户私钥
     * @throws Exception 异常信息
     */
    public static String getPrivateKeyString(String keyPath) throws Exception {
        String originalKey = FileUtil.readUtf8String(keyPath);
        return originalKey
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");
    }

    //通过公钥路径获取商户证书序列号
    public static String getSerialNumber(String serialNo, String apiclientCertUrl) {
        if (StrUtil.isEmpty(serialNo)) {
            // 获取商户证书序列号
            //apiclientCertUrl为【本地】公钥证书路径 也就是apiclient_cert.pem这个文件的路径
            X509Certificate certificate = getCertificate(FileUtil.getInputStream(apiclientCertUrl));
//            //apiclientCertUrl为【线上】公钥证书路径 也就是apiclient_cert.pem这个文件的路径
//            X509Certificate certificate = getCertificate(MultipartFileToFileUtils.getInputStreamByUrl(apiclientCertUrl));
            serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
        }
        return serialNo;
    }

    public static byte[] InputStreamTOByte(InputStream in) throws IOException {
        int BUFFER_SIZE = 4096;
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] data = new byte[BUFFER_SIZE];
        int count = -1;
        while ((count = in.read(data, 0, BUFFER_SIZE)) != -1) {
            outStream.write(data, 0, count);
        }
        data = null;
        byte[] outByte = outStream.toByteArray();
        outStream.close();
        return outByte;
    }

    public static String signRSA(String data, String priKey) throws Exception {
        //签名的类型
        Signature sign = Signature.getInstance("SHA256withRSA");
        //读取商户私钥,该方法传入商户私钥证书的内容即可
        byte[] keyBytes = org.apache.commons.codec.binary.Base64.decodeBase64(priKey);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        sign.initSign(privateKey);
        sign.update(data.getBytes("UTF-8"));
        return  Base64.encodeBase64String(sign.sign());
    }
}

微信支付API V3视频上传 实现

微信支付API V3视频上传 实现:https://blog.csdn.net/qq_44614878/article/details/121213437

 类似资料: