微信端提供接口文档:
付款到银行卡API:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=25_2
主方法:
/**
* 发送微信提现请求
*
* @param userName 用户姓名
* @param userBank 用户银行卡号
* @param userBankCode 用户银行所属编码
* @param amount 提现金额 单位分
* @param desc 描述
* @param outMoney 扣除金额
* @param idNumber 身份证号
* @param platformServiceCharge 平台手续费
* @param inWxServiceCharge 入微信平台手续费
* @param outWxServiceCharge 出微信平台手续费
*/
public Map<String, String> transferBank(String userName, String userBank, String userBankCode, String orderId, String amount, String desc,
String outMoney, String idNumber, String platformServiceCharge, String inWxServiceCharge,
String outWxServiceCharge,String userBankName) throws Exception {
int intValue = new BigDecimal(amount).multiply(new BigDecimal("100")).intValue();
String name = RSAUtils.encrypt(userName);
String bank = RSAUtils.encrypt(userBank);
String str = RequestUtils.getUUID();
Map<String, Object> params = new TreeMap<>();
params.put("mch_id", IExtractConstant.MCH_ID);
params.put("partner_trade_no", orderId);
params.put("nonce_str", str);
params.put("enc_bank_no", bank);
params.put("enc_true_name", name);
params.put("bank_code", userBankCode);
params.put("amount", intValue);
if (StringUtils.isNoneBlank(desc)) {
params.put("desc", desc);
}
//生成签名
String sign = SignUtils.getSign(params, IExtractConstant.MCH_KEY);
params.put("sign", sign);
//将请求参数转换为xml格式
String requestXml = XmlUtil.getRequestXml(params);
//请求前插入提现记录表
extractCashMapper.insertExtractCashRecord(userName, idNumber, outMoney, orderId, userBank, userBankCode,
intValue + "", desc, platformServiceCharge, inWxServiceCharge, outWxServiceCharge, "1", str,userBankName);
String weChatResponse = SendWeChatRequestUtil.sendWeChatRequest(IExtractConstant.MCH_ID,
IExtractConstant.PAY_BANK_URL,
requestXml,
IExtractConstant.CERT_PATH);
return XmlUtil.xmlToMap(weChatResponse);
}
RSAUtils:
package com.sdjx.wx.util;
import com.sdjx.wx.constant.IExtractConstant;
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import java.io.*;
import java.lang.reflect.Method;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* @Author:wpf
* @ClassName:RSAUtils
* @Date:2022/10/8
* @Description:
*/
public class RSAUtils {
/**
* rsa加密公钥
* 请求微信api获得pkcs1格式
* 通过转换工具转换成pkcs8格式
*/
private static final String PKCS8_PUBLIC = IExtractConstant.RSA_PUBLIC_KEY_PKCS8;
public static byte[] decrypt(byte[] encryptedBytes, PrivateKey privateKey, int keyLength, int reserveSize, String cipherAlgorithm) throws Exception {
int keyByteSize = keyLength / 8;
int decryptBlockSize = keyByteSize - reserveSize;
int nBlock = encryptedBytes.length / keyByteSize;
ByteArrayOutputStream outbuf = null;
try {
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
outbuf = new ByteArrayOutputStream(nBlock * decryptBlockSize);
for (int offset = 0; offset < encryptedBytes.length; offset += keyByteSize) {
int inputLen = encryptedBytes.length - offset;
if (inputLen > keyByteSize) {
inputLen = keyByteSize;
}
byte[] decryptedBlock = cipher.doFinal(encryptedBytes, offset, inputLen);
outbuf.write(decryptedBlock);
}
outbuf.flush();
return outbuf.toByteArray();
} catch (Exception e) {
throw new Exception("DEENCRYPT ERROR:", e);
} finally {
try{
if(outbuf != null){
outbuf.close();
}
}catch (Exception e){
outbuf = null;
throw new Exception("CLOSE ByteArrayOutputStream ERROR:", e);
}
}
}
public static byte[] encrypt(byte[] plainBytes, PublicKey publicKey, int keyLength, int reserveSize, String cipherAlgorithm) throws Exception {
int keyByteSize = keyLength / 8;
int encryptBlockSize = keyByteSize - reserveSize;
int nBlock = plainBytes.length / encryptBlockSize;
if ((plainBytes.length % encryptBlockSize) != 0) {
nBlock += 1;
}
ByteArrayOutputStream outbuf = null;
try {
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
outbuf = new ByteArrayOutputStream(nBlock * keyByteSize);
for (int offset = 0; offset < plainBytes.length; offset += encryptBlockSize) {
int inputLen = plainBytes.length - offset;
if (inputLen > encryptBlockSize) {
inputLen = encryptBlockSize;
}
byte[] encryptedBlock = cipher.doFinal(plainBytes, offset, inputLen);
outbuf.write(encryptedBlock);
}
outbuf.flush();
return outbuf.toByteArray();
} catch (Exception e) {
throw new Exception("ENCRYPT ERROR:", e);
} finally {
try{
if(outbuf != null){
outbuf.close();
}
}catch (Exception e){
outbuf = null;
throw new Exception("CLOSE ByteArrayOutputStream ERROR:", e);
}
}
}
public static PublicKey getPublicKey(String keyAlgorithm) throws Exception {
try
{
//加载公钥
X509EncodedKeySpec pubX509 = new X509EncodedKeySpec(decodeBase64(PKCS8_PUBLIC));
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
PublicKey publicKey = keyFactory.generatePublic(pubX509);
return publicKey;
} catch (Exception e) {
e.printStackTrace();
throw new Exception("READ PUBLIC KEY ERROR:", e);
}
}
//一下面是base64的编码和解码
public static String encodeBase64(byte[]input) throws Exception{
Class clazz=Class.forName("com.sun.org.apache.xerces.internal.impl.dv.util.Base64");
Method mainMethod= clazz.getMethod("encode", byte[].class);
mainMethod.setAccessible(true);
Object retObj=mainMethod.invoke(null, new Object[]{input});
return (String)retObj;
}
/***
* decode by Base64
*/
public static byte[] decodeBase64(String input) throws Exception{
Class clazz=Class.forName("com.sun.org.apache.xerces.internal.impl.dv.util.Base64");
Method mainMethod= clazz.getMethod("decode", String.class);
mainMethod.setAccessible(true);
Object retObj=mainMethod.invoke(null, input);
return (byte[])retObj;
}
/**
* <p>
* 按照微信要求进行加密
* </p>
*
* @param data 加密前的开户姓名/银行卡号
* @return java.lang.String 加密后秘文
* @author Winder
* @date 2021/1/26 上午10:28
*/
public static String encrypt(String data){
PublicKey pub = null;
try {
//rsa加密
pub = RSAUtils.getPublicKey("RSA");
String rsa = "RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING";
byte[] estrName = RSAUtils.encrypt(data.getBytes(), pub, 2048, 11, rsa);
//base64加密
data = Base64Utils.encodeToString(estrName);
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
String str = encrypt("张三");
System.out.println(str);
}
}
RequestUtils:
package com.sdjx.wx.util;
import com.sdjx.wx.constant.IExtractConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
/**
* @Author:wpf
* @ClassName:RequestUtils
* @Date:2022/9/30
* @Description:f发送请求工具类
*/
@Slf4j
public class RequestUtils {
/**
* 获取RSA加密公钥
* @return RSA加密公钥
*接口默认输出PKCS#1格式的公钥,商户需根据自己开发的语言选择公钥格式
*
* RSA公钥格式PKCS#1,PKCS#8互转说明
* PKCS#1 转 PKCS#8:
* openssl rsa -RSAPublicKey_in -in <filename> -pubout
*
* PKCS#8 转 PKCS#1:
* openssl rsa -pubin -in <filename> -RSAPublicKey_out
*
* PKCS#1 格式密钥:
* -----BEGIN RSA PUBLIC KEY-----
* MIIBCgKCAQEArT82k67xybiJS9AD8nNAeuDYdrtCRaxkS6cgs8L9h83eqlDTlrdw
* zBVSv5V4imTq/URbXn4K0V/KJ1TwDrqOI8hamGB0fvU13WW1NcJuv41RnJVua0QA
* lS3tS1JzOZpMS9BEGeFvyFF/epbi/m9+2kUWG94FccArNnBtBqqvFncXgQsm98JB
* 3a62NbS1ePP/hMI7Kkz+JNMyYsWkrOUFDCXAbSZkWBJekY4nGZtK1erqGRve8Jbx
* TWirAm/s08rUrjOuZFA21/EI2nea3DidJMTVnXVPY2qcAjF+595shwUKyTjKB8v1
* REPB3hPF1Z75O6LwuLfyPiCrCTmVoyfqjwIDAQAB
* -----END RSA PUBLIC KEY-----
*
* PKCS#8 格式密钥:
* -----BEGIN PUBLIC KEY-----
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT82k67xybiJS9AD8nNA
* euDYdrtCRaxkS6cgs8L9h83eqlDTlrdwzBVSv5V4imTq/URbXn4K0V/KJ1TwDrqO
* I8hamGB0fvU13WW1NcJuv41RnJVua0QAlS3tS1JzOZpMS9BEGeFvyFF/epbi/m9+
* lkUWG94FccArNnBtBqqvFncXgQsm98JB3a42NbS1ePP/hMI7Kkz+JNMyYsWkrOUF
* DCXAbSZkWBJekY4nGZtK1erqGRve8JbxTWirAm/s08rUrjOuZFA21/EI2nea3Did
* JMTVnXVPY2qcAjF+595shwUKyTjKB8v1REPB3hPF1Z75O6LwuLfyPiCrCTmVoyfq
* jwIDAQAB
* -----END PUBLIC KEY-----
*/
public static String getEncryptPublicKey() {
try {
Map<String, Object> param = new TreeMap<>();
param.put("mch_id", IExtractConstant.MCH_ID);
param.put("nonce_str", getUUID());
param.put("sign_type", "MD5");
String sign = SignUtils.getSign(param, IExtractConstant.MCH_KEY);
param.put("sign", sign);
String response = SendWeChatRequestUtil.sendWeChatRequest(IExtractConstant.MCH_ID,
IExtractConstant.GET_WECHAT_ENCRYPT_PUBLIC_KEY_URL,
XmlUtil.getRequestXml(param),
IExtractConstant.CERT_PATH);
System.out.println("-------------------");
System.out.println(response);
if (StringUtils.isNotEmpty(response)) {
Map<String, String> responseMap = XmlUtil.xmlToMap(response);
if ("SUCCESS".equals(responseMap.get("return_code")) && "SUCCESS".equals(responseMap.get("result_code"))) {
return responseMap.get("pub_key");
}else {
log.info(" weixin return fall :{}", response);
}
}
} catch (Exception e) {
log.error("调用微信企业付款至银行卡生成公钥异常:{}", e.getMessage());
}
return null;
}
/**
* uuid 随机字符串
* @return uuid 随机字符串
*/
public static String getUUID() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 变更公钥格式PKCS#1至PKCS#8
* @param publicKeyString
* @return
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
public static PublicKey getPublicKey(String publicKeyString) throws InvalidKeySpecException, NoSuchAlgorithmException {
//把RSA公钥格式PKCS#1转换成PKCS#8
org.bouncycastle.asn1.pkcs.RSAPublicKey rsaPublicKey = org.bouncycastle.asn1.pkcs.RSAPublicKey.getInstance(
org.bouncycastle.util.encoders.Base64.decode(publicKeyString));
java.security.spec.RSAPublicKeySpec publicKeySpec = new java.security.spec.RSAPublicKeySpec(rsaPublicKey.getModulus(), rsaPublicKey.getPublicExponent());
java.security.KeyFactory keyFactory = java.security.KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
return publicKey;
}
public static String readLocalPubKey(String pubKey) throws IOException {
File file = null;
try {
Path tmpFile = Files.createTempFile("payToBank", ".pem");
File file1 = tmpFile.toFile();
String absolutePath = file1.getAbsolutePath();
log.info("本地公钥路径:"+absolutePath);
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile)) {
writer.write(pubKey);
} catch (IOException e) {
e.printStackTrace();
}
List<String> lines = Files.readAllLines(Paths.get(absolutePath), StandardCharsets.UTF_8);
StringBuilder sb = new StringBuilder();
for (String line : lines) {
if (line.charAt(0) == '-') {
continue;
} else {
sb.append(line);
sb.append('\r');
System.out.println("**"+sb);
}
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}finally {
if (file != null){
file.deleteOnExit();
}
}
return null;
}
}
SignUtils:
package com.sdjx.wx.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Map;
import java.util.TreeMap;
/**
* @Author:wpf
* @ClassName:SignUtils
* @Date:2022/9/29
* @Description:获取签名工具类
*/
@Slf4j
public class SignUtils {
/**
* 获取签名方法
*
* @param requestParam 所有发送或者接收到的数据
* @param key key为商户平台设置的密钥key
* @return 签名
* 第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
* 特别注意以下重要规则:
* <p>
* ◆ 参数名ASCII码从小到大排序(字典序);
* ◆ 如果参数的值为空不参与签名;
* ◆ 参数名区分大小写;
* ◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
* ◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
* 第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。 注意:密钥的长度为32个字节。
* <p>
* ◆ key设置路径:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->设置API密钥
*/
public static String getSign(Map<String, Object> requestParam, String key) {
Map<String, Object> params = new TreeMap<>();
params.putAll(requestParam);
StringBuffer buffer = new StringBuffer();
for (Map.Entry<String, Object> param : params.entrySet()) {
if (param.getValue() != null) {
buffer.append(param.getKey() + "=" + param.getValue() + "&");
}
}
buffer.append("key=" + key);
String stringSignTemp = buffer.toString();
String signValue = DigestUtils.md5Hex(stringSignTemp).toUpperCase();
return signValue;
}
}
XmlUtil:
package com.sdjx.wx.util;
import com.jfinal.weixin.sdk.utils.XmlHelper;
import org.apache.commons.lang3.StringUtils;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.util.Map;
/**
* @Author:wpf
* @ClassName:XmlUtils
* @Date:2022/9/29
* @Description:xml和对象之间相互转化工具类
*/
public class XmlUtil {
/**
* 将请求集合拼接成xml字符串
* @param params 请求集合
* @return xml字符串
*/
public static String getRequestXml(Map<String, Object> params) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
String value = entry.getValue().toString();
// 略过空值
if (StringUtils.isBlank(value)) continue;
sb.append("<").append(key).append(">");
sb.append(entry.getValue());
sb.append("</").append(key).append(">");
}
sb.append("</xml>");
return sb.toString();
}
/**
* 微信返回xml解析成为Map集合
* @param xmlStr 待解析xml字符串
* @return 解析后Map集合
*/
public static Map<String,String> xmlToMap(String xmlStr){
XmlHelper xmlHelper = XmlHelper.of(xmlStr);
return xmlHelper.toMap();
}
public static void main(String[] args) throws IOException, SAXException {
Map<String, String> stringStringMap = xmlToMap("<xml>\n" +
"<return_code><![CDATA[SUCCESS]]></return_code>\n" +
"<return_msg><![CDATA[支付失败]]></return_msg>\n" +
"<result_code><![CDATA[FAIL]]></result_code>\n" +
"<err_code><![CDATA[FATAL_ERROR]]></err_code>\n" +
"<err_code_des><![CDATA[订单信息不一致,请先查询订单]]></err_code_des>\n" +
"<nonce_str><![CDATA[50780e0cca98c8c8e814883e5caa672e]]></nonce_str>\n" +
"<mch_id><![CDATA[2302758702]]></mch_id>\n" +
"<partner_trade_no><![CDATA[1212121221278]]></partner_trade_no>\n" +
"<amount>5000</amount>\n" +
"</xml>");
System.out.println(stringStringMap);
System.out.println(stringStringMap.get("return_msg"));
}
}
SendWeChatRequestUtil:
package com.sdjx.wx.util;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.security.KeyStore;
/**
* @Author:wpf
* @ClassName:SendWeChatRequestUtil
* @Date:2022/9/29
* @Description:携带证书发送微信请求
*/
public class SendWeChatRequestUtil {
/**
* 携带证书发送微信请求
*
* @param mchId 证书密码 默认MCHID
* @param url 请求路径
* @param data 请求体
* @param certPath 证书路径
* @return 微信请求响应结果
* <p>
* 注意PKCS12证书 是从微信商户平台-》账户设置-》 API安全 中下载的
*/
public static String sendWeChatRequest(String mchId, String url, String data, String certPath) throws Exception {
//指定读取证书格式为PKCS12
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//读取证书文件
FileInputStream inputStream = new FileInputStream(certPath);
try {
//指定PKCS12的密码
keyStore.load(inputStream, mchId.toCharArray());
} finally {
inputStream.close();
}
//信任自己的CA和所有自签名证书
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
//仅允许TLSv1协议
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
try {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(data, "UTF-8"));
CloseableHttpResponse response = httpclient.execute(httpPost);
try {
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
EntityUtils.consume(entity);
return jsonStr;
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
}
PKCS#1转成PKCS#8在线工具 网址 http://www.metools.info/code/c85.html