最近有个项目要开发微信支付之服务商模式,作为服务商提供给商户入驻则需要接入微信特邀商户入驻功能。
这里面恶心的不止是接微信特邀商户入驻申请API(将近100个字段),最最恶心的是有个微信支付业务指定商户需要使用图片或视频的API,跟着API文档走你会发现一直卡在两个请求错误之间。
错误1:图片sha256值计算有误,请检查算法,重新计算后提交
错误2:签名错误,验签失败
实际上是跟着API文档和SDK接入的,这SDK和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视频上传 实现:https://blog.csdn.net/qq_44614878/article/details/121213437