主要所需:1、微信商户平台的证书apiclient_cert.pem 2、微信商户平台证书的密钥apiclient_key.pem 3、微信商户平台的证书的序列号
一、转账所需字段
public class WxTransferAccounts {
private String appid;// 小程序ID
private String out_batch_no;// 商家批次订单号 由数字、大小写字母组成[1,32]
private String batch_name;// 商家批次名称 示例值:2019年1月深圳分部报销单[1,32]
private String batch_remark;//批次备注 , [1,32]
private int total_num;// 转账总笔数 这个总笔数要等于明细笔数的汇总
private Integer total_amount;// 转账总金额 ,单位为分 这个总金额要等于明细金额的汇总
private List<TransferAccountsArray> transfer_detail_list;//收款方明细
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getOut_batch_no() {
return out_batch_no;
}
public void setOut_batch_no(String out_batch_no) {
this.out_batch_no = out_batch_no;
}
public String getBatch_name() {
return batch_name;
}
public void setBatch_name(String batch_name) {
this.batch_name = batch_name;
}
public String getBatch_remark() {
return batch_remark;
}
public void setBatch_remark(String batch_remark) {
this.batch_remark = batch_remark;
}
public int getTotal_num() {
return total_num;
}
public void setTotal_num(int total_num) {
this.total_num = total_num;
}
public Integer getTotal_amount() {
return total_amount;
}
public void setTotal_amount(Integer total_amount) {
this.total_amount = total_amount;
}
public List<TransferAccountsArray> getTransfer_detail_list() {
return transfer_detail_list;
}
public void setTransfer_detail_list(List<TransferAccountsArray> transfer_detail_list)
{
this.transfer_detail_list = transfer_detail_list;
}
}
public class TransferAccountsArray {
private String out_detail_no;// 商家明细单号[1,32]
private String openid;//用户openid
private String user_name;// 用户真实姓名,要与微信号绑定的身份实名,超过2000元时必填 需进行加密处理;如低于2000元的转账,则可以不需要此字段
private String transfer_remark;//转账备注 [1,32]
private int transfer_amount;// 转账金额
public String getOut_detail_no() {
return out_detail_no;
}
public void setOut_detail_no(String out_detail_no) {
this.out_detail_no = out_detail_no;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getUser_name() {
return user_name;
}
public void setUser_name(String user_name) {
this.user_name = user_name;
}
public String getTransfer_remark() {
return transfer_remark;
}
public void setTransfer_remark(String transfer_remark) {
this.transfer_remark = transfer_remark;
}
public int getTransfer_amount() {
return transfer_amount;
}
public void setTransfer_amount(int transfer_amount) {
this.transfer_amount = transfer_amount;
}
}
二、转账接口调用前准备
WxTransferAccounts paramWxTransferAccounts=new WxTransferAccounts();
paramWxTransferAccounts.setAppid("个人的小程序appid,要与商户绑定");
paramWxTransferAccounts.setBatch_name("2022.09.29测试新版转账");
paramWxTransferAccounts.setBatch_remark("2022.09.29测试新版转账");
paramWxTransferAccounts.setOut_batch_no("商户订单号 32位 自己生成");
paramWxTransferAccounts.setTotal_amount(100);
paramWxTransferAccounts.setTotal_num(1);
TransferAccountsArray paramTransferAccountsArray=new TransferAccountsArray();
paramTransferAccountsArray.setOpenid("收款人的opendid");
paramTransferAccountsArray.setOut_detail_no("明细订单号 32位 自己生成");
paramTransferAccountsArray.setTransfer_amount(100);
paramTransferAccountsArray.setTransfer_remark("2022.09.29测试新版转账");
paramTransferAccountsArray.setUser_name(rsaEncryptOAEP("真实姓名", certificate));//如果转账低于2000,无需这个字段,否则需要进行隐私信息进行加密处理,加密代码在后面
List<TransferAccountsArray> listAccounts=new ArrayList<>();
listAccounts.add(paramTransferAccountsArray);
paramWxTransferAccounts.setTransfer_detail_list(listAccounts);
三、隐私信息安全加密
//要先获取微信支付平台的公钥证书,通过api获取;而后可以考虑放redis
String mch_id="你自己的商户号";
String privatekeypath="商户平台证书密钥的路径";
String nonce_str=StrUtil.getRandomStringByLength(32);//随机32位字符串
String body="";
long timestamp = System.currentTimeMillis() / 1000;
String orgSignText = "GET\n"
+ "/v3/certificates\n"
+ timestamp + "\n"
+ nonce_str + "\n"
+ body + "\n";
String signStr=VechatPayV3Util.sign(orgSignText.getBytes("utf-8"), privatekeypath);//获得签名
String wechatPayserialNo="微信商户平台证书的序列号";
String auth = "WECHATPAY2-SHA256-RSA2048 "
+ "mchid=\""+mch_id+"\",nonce_str=\""
+ nonce_str + "\",timestamp=\"" + timestamp
+ "\",serial_no=\"" + wechatPayserialNo + "\",signature=\"" + signStr + "\"";
//获取微信支付平台公钥证书
String platform_publickey = HttpUtil.sendGetRequest("https://api.mch.weixin.qq.com/v3/certificates", auth,null);
//获取的微信支付平台公钥证书是一个json字符串,自行转成json对象
//获得的公钥证书是加密的,需要用apiV3的密钥进行解密
String publickey=decryptResponseBody(tempWxpublicKeyData);//tempWxpublicKeyData 这个对象就是取回来的公钥字符串转换的
ByteArrayInputStream inputStream = new ByteArrayInputStream(publickey.getBytes(StandardCharsets.UTF_8));
X509Certificate certificate2=getCertificate(inputStream);
rsaEncryptOAEP("用户的真实姓名", certificate2)//加密隐私信息 这里我用来加密转账所需的姓名
/**
* 解密响应体. 得到微信平台证书公钥,解密后的字符串即为公钥字符串
*
* @param apiV3Key API V3 KEY API v3密钥 商户平台设置的32位字符串
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
* @throws GeneralSecurityException the general security exception
*/
public static String decryptResponseBody(WxpublicKeyData tempWxpublicKeyData) {
//tempWxpublicKeyData 这个对象就是取回来的公钥字符串转换的,有时回取回多条公钥,取时间最新的
String apiV3Key="微信商户平台apiV3的密钥,记得去微信商户平台设置";
String associatedData=tempWxpublicKeyData.getEncrypt_certificate().getAssociated_data();
String nonce=tempWxpublicKeyData.getEncrypt_certificate().getNonce();
String ciphertext=tempWxpublicKeyData.getEncrypt_certificate().getCiphertext();
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);
}
}
/**
* 获取证书
*
* @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 data 待加密数据
* @param certificate 平台公钥证书
* @return 加密后的数据
* @throws Exception 异常信息
*/
public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
byte[] cipherData = cipher.doFinal(dataByte);
// String s = new String(cipherData);
return java.util.Base64.getEncoder().encodeToString(cipherData);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的证书", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
}
}
四、微信商家转账到零钱接口调用
//发起转账操作
//certificate2.getSerialNumber().toString(16).toUpperCase() :微信支付平台证书的序列号
//wechatPayserialNo :微信商户平台证书的序列号
//mch_id:商户号
//privatekeypath:微信商户平台密钥地址
//注:我这边传递了两个证书序列号,实际post时只需要一个,做测试发现当有传递加密隐私信息时,序列号用微信支付平台证书的序列号;没有传递加密隐私信息时,则用微信商户平台证书的序列号即可
String transferurl="https://api.mch.weixin.qq.com/v3/transfer/batches";
String resStr = HttpUtil.postTransBatRequest(
transferurl,
JSONObject.toJSONString(paramWxTransferAccounts),
certificate2.getSerialNumber().toString(16).toUpperCase(),
wechatPayserialNo,
mch_id,
privatekeypath);
HttpUtil的公用类
import java.io.IOException;
import java.util.HashMap;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import ch.qos.logback.classic.Logger;
/**
* 微信支付专用类 请求操作方法 2022.09.29 新版商家转账到零钱
*
* @author Administrator
*/
public class HttpUtil {
/**
* 发起批量转账API 批量转账到零钱
*
* @param requestUrl
* @param requestJson 组合参数
* @param wechatPayserialNo 商户证书序列号
* @param mchID4M 商户号
* @param privatekeypath 商户私钥证书路径
* @return
*/
public static String postTransBatRequest(
String requestUrl,
String requestJson,
String platform_wechatPayserialNo,
String wechatPayserialNo,
String mchID4M,
String privatekeypath) {
CloseableHttpClient httpclient = HttpClients.createDefault();
CloseableHttpResponse response = null;
HttpEntity entity = null;
try {
//商户私钥证书
HttpPost httpPost = new HttpPost(requestUrl);
// NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误
httpPost.addHeader("Content-Type", "application/json");
httpPost.addHeader("Accept", "application/json");
//httpPost.addHeader("Wechatpay-Serial", wechatPayserialNo);
httpPost.addHeader("Wechatpay-Serial", platform_wechatPayserialNo);//用了隐私信息加密时,上传的微信支付平台公钥的序列号
//-------------------------核心认证 start-----------------------------------------------------------------
String strToken = VechatPayV3Util.getToken("POST",
"/v3/transfer/batches",
requestJson,mchID4M,wechatPayserialNo, privatekeypath);
System.out.println("微信转账token "+strToken);
// 添加认证信息
httpPost.addHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048" + " "
+ strToken);
//---------------------------核心认证 end---------------------------------------------------------------
httpPost.setEntity(new StringEntity(requestJson, "UTF-8"));
//发起转账请求
response = httpclient.execute(httpPost);
entity = response.getEntity();//获取返回的数据
return EntityUtils.toString(entity);
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
} finally {
// 关闭流
}
return null;
}
/**
* 发送HTTP_GET请求
*
* @see 该方法会自动关闭连接,释放资源
* @param reqURL
* 请求地址(含参数)
* @param decodeCharset
* 解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
* @return 远程主机响应正文
*/
public static String sendGetRequest(String reqURL,String auth,String decodeCharset) {
long responseLength = 0; // 响应长度
String responseContent = null; // 响应内容
HttpClient httpClient = new DefaultHttpClient(); // 创建默认的httpClient实例
HttpGet httpGet = new HttpGet(reqURL); // 创建org.apache.http.client.methods.HttpGet
httpGet.addHeader("Authorization", auth);
httpGet.addHeader("Accept", "application/json");
httpGet.addHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
try {
HttpResponse response = httpClient.execute(httpGet); // 执行GET请求
HttpEntity entity = response.getEntity(); // 获取响应实体
if (null != entity) {
responseLength = entity.getContentLength();
responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
EntityUtils.consume(entity); // Consume response content
}
} catch (ClientProtocolException e) {
System.out.println("该异常通常是协议错误导致,比如构造HttpGet对象时传入的协议不对(将'http'写成'htp')或者服务器端返回的内容不符合HTTP协议要求等,堆栈信息如下");
} catch (ParseException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println("该异常通常是网络原因引起的,如HTTP服务器未启动等,堆栈信息如下");
} finally {
httpClient.getConnectionManager().shutdown(); // 关闭连接,释放资源
}
return responseContent;
}
}
VechatPayV3Util公用类
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.List;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import cn.eeyycc.mina.framework.wxpay.model.WxpublicKeyData;
public class VechatPayV3Util {
/**
*
* @param method 请求方法 post
* @param canonicalUrl 请求地址
* @param body 请求参数
* @param merchantId 这里用的商户号
* @param certSerialNo 商户证书序列号
* @param keyPath 商户证书地址
* @return
* @throws Exception
*/
public static String getToken(
String method,
String canonicalUrl,
String body,
String merchantId,
String certSerialNo,
String keyPath) throws Exception {
String signStr = "";
//获取32位随机字符串
String nonceStr = getRandomString(32);
//当前系统运行时间
long timestamp = System.currentTimeMillis() / 1000;
if (StringUtils.isEmpty(body)) {
body = "";
}
//签名操作
String message = buildMessage(method, canonicalUrl, timestamp, nonceStr, body);
//签名操作
String signature = sign(message.getBytes("utf-8"), keyPath);
//组装参数
signStr = "mchid=\"" + merchantId + "\",timestamp=\"" + timestamp+ "\",nonce_str=\"" + nonceStr
+ "\",serial_no=\"" + certSerialNo + "\",signature=\"" + signature + "\"";
return signStr;
}
public static String buildMessage(String method, String canonicalUrl, 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";
}
public static String sign(byte[] message, String keyPath) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(keyPath));
sign.update(message);
return Base64.encodeBase64String(sign.sign());
}
/**
* 微信支付-前端唤起支付参数-获取商户私钥
*
* @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+", "");
//System.out.println("--------privateKey---------:"+privateKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 获取随机位数的字符串
* @param length
* @return
*/
public static String getRandomString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
转账成功示例:
获取微信支付平台证书时,解密可能会报错密钥
java.security.InvalidKeyException: Illegal key size
解决方案:(异常: java.security.InvalidKeyException: Illegal key size - 萌新啊萌新是我 - 博客园)
至于报错IP未设置,api支付未开启,余额不足啥的,都在微信商户平台进行设置即可
开发参考:微信支付 发起商家转账API 2022年v3 transfer batches_早起的年轻人的博客-CSDN博客_微信转账api
java开发 微信商家转账到零钱,发起商家转账API,微信支付_海贝里的灰尘的博客-CSDN博客_java实现微信转账