主要所需: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;//用户openidprivate 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.HttpGethttpGet.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实现微信转账
Java中的微信支付(2):API V3 微信平台证书的获取与刷新 - 走看看
微信支付V3获取平台证书并解密平台证书详细流程_低调使人进步的博客-CSDN博客_微信支付平台证书