微信商家转账到零钱,企业付款升级避坑
1,概述
原微信企业付款升级成为了商家转账到零钱接口,之前的商户开通了企业付款接口的还能继续用(但是微信在升级指引中说后续可能下架企业付款),新的商户只能开通商家转账到零钱接口了,商家转账到零钱官方文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml
接口说明
**适用对象:**直连商户
**请求URL:**https://api.mch.weixin.qq.com/v3/transfer/batches
**请求方式:**POST
接口限频: 单个商户 50QPS,如果超过频率限制,会报错FREQUENCY_LIMITED,请降低频率请求。
**是否需要证书:**是
2.发起商家转账到零钱
网上有很多代码java代码示例,官方文档也有demo,所以本文就不贴代码了,本文只是想说一些开发过程中需要注意的点和一些坑。
1.证书问题
商家转账到零钱是微信支付V3版本,不同于之前的企业付款(v2版本),微信支付v3版本用到了两个证书,一个是微信支付平台证书,一个是商户api证书,(原来企业付款只需要商户api证书)。
先说两种证书区别,微信支付平台证书是用来给敏感字段加密的(比如姓名字段),商户api证书是用来给参数加签名用的,因为v3支付需要在请求头上加Authorization
字段,官方文档都有,接下来具体说明:
微信支付平台证书
目前微信支付平台证书只能通过api接口下载,官方文档地址先送上:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
可以通过postman请求,获取微信返回的参数,
{
"original_type": "transaction", // 加密前的对象类型
"algorithm": "AEAD_AES_256_GCM", // 加密算法
// Base64编码后的密文
"ciphertext": "...",
// 加密使用的随机串初始化向量)
"nonce": "...",
// 附加数据包(可能为空)
"associated_data": ""
}
其中微信返回的内容中,ciphertext
字段就是微信支付平台证书的内容,此时这个内容是加密过的,需要解密才行,解密需要用到微信支付apiV3密钥,设置密钥的方法:在【商户平台】->【API安全】的页面设置该密钥,还要说明下,apiv3密钥一定要设置,不然接口调不了,而且设置完了密钥还要设置ip白名单,加入了白名单的ip才能调用接口。
平台证书解密,java demo
public static String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) {
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);
}
}
String apiV3="aaa";//apiV3密钥
String associatedData="certificate";//微信平台证书下载接口返回的附加数据包(可能为空)
String nonce="bbbb";//微信平台证书下载接口返回的随机数
String ciphertext="aaa";//微信平台证书下载接口返回的加密串
String pubKey=AesUtil.decryptResponseBody(apiV3,associatedData,nonce,ciphertext);
System.out.println(pubKey);
这时打印出来的是
-----BEGIN CERTIFICATE-----
aaaaaaa
-----END CERTIFICATE-----
的证书密钥字符串,我们可以复制到记事本保存为PublicKey.pem文件,此时这个pem文件就是微信支付平台证书pem文件,如果需要加密,
//其中message是要加密的内容,keyPath是微信支付平台证书pem文件路径
public static String rsaEncryptOAEP(String message,String keyPath)
throws IllegalBlockSizeException, IOException, CertificateException {
ClassPathResource classPathResource = new ClassPathResource(keyPath);
InputStream inputStream = classPathResource.getInputStream();
X509Certificate certificate = getCertificate(inputStream);
PublicKey publicKey = certificate.getPublicKey();
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] data = message.getBytes("utf-8");
byte[] cipherdata = cipher.doFinal(data);
return 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字节");
}
}
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);
}
}
商户api证书
说到商户证书真的踩了很多坑,原来我做企业付款用的是.p12商户证书,而商家转账用的是.pem格式的商户证书,这块花费了我很多时间。
微信商户证书总共有三种格式,证书pkcs12格式、证书pem格式、证书密钥pem格式,官方文档说明:https://kf.qq.com/faq/180824JvUZ3i180824YvMNJj.html,
商家转账用的是证书密钥pem格式,那我原来如果只有.p12证书也不要紧(有pem证书的跳过),可以使用p12证书生成签名使用的api证书私钥文件apiclient_key.pem
方法:使用openssl命令,openssl可以百度安装下
openssl pkcs12 -in 商户证书.p12 -out apiclient_key.pem -nodes -nocerts
上面的命令可以生成商户证书私钥pem文件,可以用文本编辑器打开,可以看到内容是
Bag Attributes
localKeyID: ******
friendlyName: Tenpay Certificate
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
*****abcdxxxxxx-----END PRIVATE KEY-----
这样的内容,这边把开头的bag内容去掉,只保留
-----BEGIN PRIVATE KEY-----
*****abcdxxxxxx-----END PRIVATE KEY-----
的内容,然后用此私钥证书pem文件来给参数签名。
说到签名,微信支付v3需要签名的参数,签名串一共有五行,每一行为一个参数。行尾以 \n
(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n
结束,也需要附加一个\n
,官方文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n
签名生成后,就需要在请求头放Authorization
信息了,Authorization: 认证类型 签名信息
认证类型,目前为WECHATPAY2-SHA256-RSA2048
特别说明,Authorization的时间戳和随机串要和生成签名用到的时间戳和随机串一样
签名信息:
- 发起请求的商户(包括直连商户、服务商或渠道商)的商户号
mchid
- 商户API证书
序列号serial_no
,用于声明所使用的证书 - 请求随机串
nonce_str
- 时间戳
timestamp
- 签名值
signature
商户api证书序列号可以通过商户后台查看,也可以通过商户证书pem查看,命令:openssl x509 -in 商户证书.pem -noout -serial
转账时需要加请求头:
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("content-type", "application/json;charset=UTF-8");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048"+" "+token);//token为签名
httpPost.addHeader("Wechatpay-Serial", "aaa");//如果要加密姓名字段,需要多送个Wechatpay-Serial,不加密可以不送