一、接入准备
微信支付官网:https://pay.weixin.qq.com
注册微信商家号,在账号中心申请API证书及设置APIv3密钥 ,产品中心申请开通Native支付,AppId账号管理关联AppId(微信体系内应用ID)
申请通过后,将得到对接过程中所需要的重要参数:
- 账号信息内的微信支付商户号mchid
- 微信体系内APPID
- APIv3密钥
- API证书序列号
- API证书压缩包
二、创建支付订单
官方Native下单API文档地址
项目内引入 wechatpay-apache-httpclient 依赖(微信支付提供)
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.4</version>
</dependency>
创建订单测试类
public class CreateOrderTest {
private String privateKey = "MIIEvQ"; // API证书压缩包内apiclient_key.pem文件内容 -> 私钥字符串
private String appId = "wx21121323682f"; // 微信体系 AppId
private String mchId = "1617212105"; // 账号信息 - 微信支付商户号
private String mchSerialNo = "7DCA2B127B9DDD9A87612172B8EB5E8"; // API证书管理 API证书序列号
private String apiV3Key = "n21eddqwdqwde2easjdhas213dfas123"; // APIv3密钥
private CloseableHttpClient httpClient;
@Before
public void nativeRequestTest() throws UnsupportedEncodingException {
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes("utf-8"));
// 初始化httpClient
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
}
/**
* 微信native下单接口
*/
@Test
public void create() throws Exception{
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 下单接口金额单位为分
String money = "0.01";
BigDecimal multiply = new BigDecimal(money).multiply(new BigDecimal("100"));
int i = multiply.intValue();
// 商家系统生成订单号 唯一且不可重复,即使关闭微信订单后相同订单号也无法再次创建订单
String out_trade_no = "211014247fb24262a2cc3c8548c24312";
// 请求body参数
// 支付成功回调通知接口 notify_url,必须为https
String reqdata = "{" +
"\"time_expire\": \"2022-10-21T15:38:00+08:00\"," +
"\"amount\": {" +
"\"total\": " + i + "," +
"\"currency\": \"CNY\"" +
"}," +
"\"mchid\": \"" + mchId + "\"," + // 商家号
"\"out_trade_no\": \"" + out_trade_no + "\", " + // 系统订单号
"\"appid\": \"" + appId + "\"," +
"\"description\": \"" + "测试支付" + "\"," +
"\"attach\": \"自定义数据说明\"," +
"\"notify_url\": \"https://143.232.433.42:8285/wxPay/call\" " +
"}";
StringEntity entity = new StringEntity(reqdata, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
}
三、关闭订单
/**
* 关闭订单
* 204 订单关闭成功返回状态204 无响应报文
* success
* @throws IOException
*/
@Test
public void close() throws IOException {
HttpPost post = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/211014247fb24262a2cc32118c24312/close");
// 请求body参数
String reqdata = "{" +
"\"mchid\": \"" + mchId + "\"" +
"}";
StringEntity entity = new StringEntity(reqdata, "utf-8");
entity.setContentType("application/json");
post.setEntity(entity);
post.setHeader("Accept", "application/json");
// 完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码:" + statusCode);
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
}
四、查询订单信息
/**
* 查询订单
* success,return body = {"amount":{"payer_currency":"CNY","total":1},"appid":"wxe406f9ceabc6682f","mchid":"1617009205","out_trade_no":"211014247fb21231248c24312","promotion_detail":[],"scene_info":{"device_id":""},"trade_state":"NOTPAY","trade_state_desc":"订单未支付"}
* @throws Exception
*/
@Test
public void query()throws Exception{
HttpGet get = new HttpGet("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/211014247fb24262a2cc3c8548c24312?mchid=" + mchId);
get.setHeader("Accept", "application/json");
CloseableHttpResponse response = httpClient.execute(get);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
}
五、支付成功回调通知报文解析
/**
* 微信支付回调响应报文解密工具类
*/
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
public class DecodeResponseBodyTest {
private String apiV3Key = "ni3221tezho43she43n434123"; // APIv3密钥
/**
* 微信支付回调报文解密
*
* @throws GeneralSecurityException
* @throws IOException
*/
@Test
public void decode() throws GeneralSecurityException, IOException {
// 回调通知报文
String response = "{\"id\":\"5ae3a07e-0025-5e12-b959-4f7abe76ffbc\",\"create_time\":\"2022-10-21T11:54:50+08:00\",\"resource_type\":\"encrypt-resource\",\"event_type\":\"TRANSACTION.SUCCESS\",\"summary\":\"支付成功\",\"resource\":{\"original_type\":\"transaction\",\"algorithm\":\"AEAD_AES_256_GCM\",\"ciphertext\":\"K/IPHngj23SQvCcmplRuYRt7qjMqHQ9ALG5ou53wG/c331rFf8YjWo+ZDMEgxdRHsmdW4nxrmN7z62h4b98Ka4eCyuxaVaOc1Ddji28cP+u7XFTBWB8Ac9VqVmjQRlv+uM/Z0EIjYYrTx547Bw9soF/sCX9QPtmL9QzvtHlOocHSVDpPgLTrLSR0ruJL+O+PXQ8GQGv/6Jy+bRLmF/5/Lr2CkTybTP2XMfrJevTrKg/OXEHtqtTRdufgW13KdZ7wEHWljZyRM5TqnZOrk04A82+KNXGgaWh5HBtjQ7E+R1wdIiD3+o/aT82M1/FI7eJLRkeiBzIAEkjmFMGkygjZ2+zWpResInCL3VcHIFbl99NKqYDHNAXyUoeARvuI1uwHUBhQyQeF5TQVtNBjiM54K5U5uWTBu2rbsNB5OQU3sFPXzWJTCNKf3UnxMJW5ADpl2AbKzOiBj8bW4fZEy+WR+d2C83PS53R0hXz4g4JEX7Mi5ePP90OIiqI18gmGhfrmaJ9T8tzR8Ik9JSHvaj21coJgf7T4DRKg632Q0rCa5kl8h1saGHthO43jhqymVBDGp/kD6PqU1jvWanuVzIWYKSRCwnrqbgGXPmaupWX8dwphc1sTVA==\",\"associated_data\":\"transaction\",\"nonce\":\"AZ28LA0bMlB5\"}}";
JSONObject jsonObject = JSONObject.parseObject(response);
JSONObject resource = jsonObject.getJSONObject("resource");
// 直接使用 resource.getBytes("nonce") 是错误的
String associated_data = resource.getString("associated_data");
String nonce = resource.getString("nonce");
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes());
String decrypt = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), resource.getString("ciphertext"));
System.out.println("解密后得内容:" + decrypt);
}
}
六、通知签名验证
加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户通过验证签名,以确认请求来自微信,而不是其他的第三方。
/**
* 获取请求头认证签名信息工具类,用于获取商户微信平台证书列表
*/
public class AuthorizationToken {
private String mchId = "1617009205"; // 账号信息 - 微信支付商户号
private String mchSerialNo = "7DCA2B127B9DDD9A876153F3255F3F472B8EB5E8"; // 商户API证书序列号
private String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDfacUE1AeWa4TT" +
"ukE/bsjulIBn7VMvsd+8ASZfUqWs5P4MW0Bqp/U9tb1iIO8ECGnK8csC80/peQe7" +
"SSjdaBFLTWhCOn/4QSjY5wVMNxL9YW17rO2E1YHg+uYpB6i950H27vTh18EOtyeG" +
"g2tgwGTr3/IuPeY5Cy/ZX4VEWnondYGhKWjJYXYtV1ypeBCkLe/A9zoyiIQUUQiz" +
"YYibiU/Ow190Iywzc4oKT1K4lXGdai9OZHzkQorOBkCbx2/9vyLvvUjyfa6RtMWv" +
"qpZuKqjEecQYtqJ6VvJ1N+uKMGDpyChSqBHxG0LT0K5Wu9Gtg5Y+NrYbzMThZUt5" +
"/aJAF0L7AgMBAAECggEARz2RB6Mc8EhEyMchuzp2dC2CbKFu30yXDXpIZCkUj3dN" +
"017dwaThPNZRF5Ns5BpSsdY8aCpyFv7zCjOgBkoDCcIbNtM0r1MH1XKFa/I76fRB" +
"VyijbLIwgi8/aWH52uR9UmKMT9/evfSFdA1AFlADXnvA3CH84b/BeE1PT6aSQTZM" +
"nZ8G7sgcQe8oBz+7oxhJl+wdDscerPg9YHRid/G3L/sjhKjOaMsBJar7gOhYNsop" +
"pfbv51gl8v3iT6luwEkf5zmsM5GfXcyKYKZgKjlI3wOWe2QsvcaprsVQhoMETV1/" +
"I5/KNWIr8sNur7EcEz0TBjLjqNh1pq1M+wTnIi25kQKBgQDzpdt0q5+mdht+ehbm" +
"2X0TWLm89zPOlUZmq+X0UqD5rYTmZ8JHZuaicVaRqUQoTV18IgiHn7lyPyGFYO20" +
"0ROl3aVLrpIqTXzmOYUM1AMtEDr6Gmky8RxPaUskMeOe3liPeF/T+AbYw5M9QRPo" +
"CUTOkldu+w3vKauNPPM6puAmZwKBgQDqvUysPFhAu8YGCqHuJhOcoZkhqh4UrW7o" +
"MEcv4UVAOwnaY55srkBLR5TN8rpaWcvFCD02Vg1nge9fdWiV1wtH81Oz5bIA+nM1" +
"qYnOSu2Z1dqEU/iEaR3t+XCSkvuaqydQ6DYW9GVFt/Mj3xjV9r864QnDMuxGspGk" +
"EY3990jaTQKBgQCjwRJpLLwldfXuoIHp77zXludm8MJaEwv5D4mDF1Hn3U6YSJ5T" +
"vP4/qWskhR4w9CZjur/+30QVXAbcjRPWVjsdXIWvAwpr8h6C4Z/hylDEJcdttviD" +
"a3e6i6scDYfNi+T7sEy/u1BmubOpFKcbabdcGxE2nvdziY8qYw+amPPH+wKBgCfU" +
"Vt4inxbcxYzg4Pj3nPxGryT3KIN5qgfbqTiGkKmFWvajUI5AQsiDLMyFEvmhouGb" +
"tEcz8rJNacBYu5YxFsjukJVFtB5WYJYKXkeSjx47Gwi49sIA1AM8/8zfA7IKuHER" +
"9ZuPfF+IBslfYWdspqXm6TElwtF8GxoroFwnSUVBAoGAS+v+LYcrp4zXMuDk9o9h" +
"3YhzDtedsmTEgR3CHs4hgltdX/jobDfNUk7QVe2ild1O3ULL83r09lf0GoBGxcxt" +
"l5aGMEsdYLQPROanUZqGihBcWqW3P2Sde4rTgFvi+X14JwFbcmn21ddY9bQbbAzv" +
"scKClgUPJMk5d0ZeAeJ/cR8="; // 商户API证书私钥字符串
public String getToken(String method, String url, String body) throws Exception {
String nonceStr = UUID.randomUUID().toString().replace("-","");
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "mchid=\"" + mchId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + mchSerialNo + "\","
+ "signature=\"" + signature + "\"";
}
String sign(byte[] message) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
sign.initSign(merchantPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
String buildMessage(String method, String url, long timestamp, String nonceStr, String body) {
return method + "\n"
+ url + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
}
/**
* 验证回调通知签名测试类
*/
public class VerifySignatureTest {
private String apiV3Key = "nin3232312123123432432anyan123"; // APIv3密钥
@Test
public void verify() throws Exception {
// 支付成功回调参数
String time = "1664344490";
String nonce = "fNPfxxlVyJp21232fV93ri1aW";
String serial = "745AB64ADA0A21221CBEAE7B23DB4C08C4CE";
String signature = "k/+nNrE6yRLTzMfriBYA6L1MQc12is/qOmdIKogkIy/FrdavmRB2X4F77gBuhlEG8RM156nqxw+THAEEimuu0nuErUe3GeKRkdU2xG6/aiulQ9dXx8YRr14z15c/vEjEhuN/YhhK9RmRoBACrVcv9+5OwULI1872/CEdhj8DC2WwiRC4nldo+O3jPwS3ma1qkkF6hBYQmXYFbRP5zQRSDsk6FLUq5jq++oHk4kYjIlHR7o1FpvMj1y2tHzOQTOy8pFta3RVoS5iJ5oxzA+9wSwXsMsa1rBOn6xD3TzmqFnoiOd/1bvGbqkFXMIav01AUlrYHzdxokEqS484HWgbeIw==";
String body = "{\"id\":\"5ae3a07e-0025-5e12-b959-4f7abe76ffbc\",\"create_time\":\"2022-10-21T11:54:50+08:00\",\"resource_type\":\"encrypt-resource\",\"event_type\":\"TRANSACTION.SUCCESS\",\"summary\":\"支付成功\",\"resource\":{\"original_type\":\"transaction\",\"algorithm\":\"AEAD_AES_256_GCM\",\"ciphertext\":\"K/IPHngj23SQvCcmplRuYRt7qjMqHQ9ALG5ou53wG/c331rFf8YjWo+ZDMEgxdRHsmdW4nxrmN7z62h4b98Ka4eCyuxaVaOc1Ddji28cP+u7XFTBWB8Ac9VqVmjQRlv+uM/Z0EIjYYrTx547Bw9soF/sCX9QPtmL9QzvtHlOocHSVDpPgLTrLSR0ruJL+O+PXQ8GQGv/6Jy+bRLmF/5/Lr2CkTybTP2XMfrJevTrKg/OXEHtqtTRdufgW13KdZ7wEHWljZyRM5TqnZOrk04A82+KNXGgaWh5HBtjQ7E+R1wdIiD3+o/aT82M1/FI7eJLRkeiBzIAEkjmFMGkygjZ2+zWpResInCL3VcHIFbl99NKqYDHNAXyUoeARvuI1uwHUBhQyQeF5TQVtNBjiM54K5U5uWTBu2rbsNB5OQU3sFPXzWJTCNKf3UnxMJW5ADpl2AbKzOiBj8bW4fZEy+WR+d2C83PS53R0hXz4g4JEX7Mi5ePP90OIiqI18gmGhfrmaJ9T8tzR8Ik9JSHvaj21coJgf7T4DRKg632Q0rCa5kl8h1saGHthO43jhqymVBDGp/kD6PqU1jvWanuVzIWYKSRCwnrqbgGXPmaupWX8dwphc1sTVA==\",\"associated_data\":\"transaction\",\"nonce\":\"AZ28LA0bMlB5\"}}";
// 1.生成签名认证信息 Authorization
String token = getAuthorizationToken();
// 2.根据签名获取平台证书
String certificates = getCertificatesList(token);
// 3.选择正确的一个证书字符串
String certificateJson = chooseCertificate(certificates, serial);
// 4.从报文中解析出证书字符串
String certificate = resolveCertificate(certificateJson);
// 5.将证书字符串转为证书对象
Certificate certificateObj = convertCertificate(certificate);
// 6.拼接验签名串,字符串尾部一定也需要添加\n
String str = Arrays.asList(time, nonce, body).stream().collect(Collectors.joining("\n"))+"\n";
System.out.println("验签字符串:" + str);
// 7.将返回的应答签名进行Base64解码
byte[] signatureByte = Base64.getDecoder().decode(signature);
// 8.验证签名
boolean verifyResult= verifySignature(signatureByte, str, certificateObj);
System.out.println("验签结果:" + verifyResult);
}
/**
* 从报文中选择一个验签的证书
* @param certificates
* @param Serial
* @return
*/
public String chooseCertificate(String certificates,String Serial){
JSONObject jsonObject = JSONObject.parseObject(certificates);
JSONArray data = jsonObject.getJSONArray("data");
for (Object certificate : data) {
jsonObject = (JSONObject) certificate;
if (Serial.equals(jsonObject.getString("serial_no"))){
return JSONObject.toJSONString(jsonObject);
}
}
return null;
}
/**
* 验证签名
* @param signature 微信返回的签名
* @param signStr 验签字符串
* @param certificate 微信平台证书对象
* @return
* @throws Exception
*/
public boolean verifySignature(byte[] signature, String signStr,Certificate certificate) throws Exception {
// 加载SHA256withRSA签名器
Signature sign = Signature.getInstance("SHA256withRSA");
// 用微信平台证书公钥对签名器进行初始化
sign.initVerify(certificate);
// 把我们构造的验签名串更新到签名器中
sign.update(signStr.getBytes(StandardCharsets.UTF_8));
boolean verify = sign.verify(signature);
return verify;
}
/**
* 证书字符串转换为证书对象
* @param certificates
* @return
* @throws CertificateException
*/
public Certificate convertCertificate(String certificates) throws CertificateException {
//获取平台证书
CertificateFactory cf = CertificateFactory.getInstance("X509");
ByteArrayInputStream inputStream = new ByteArrayInputStream(certificates.getBytes(StandardCharsets.UTF_8));
X509Certificate x509Certificate = (X509Certificate) cf.generateCertificate(inputStream);
return x509Certificate;
}
/**
* 解析证书Json字符串得到平台证书字符串
* @param certificateJson
* @return
* @throws Exception
*/
public String resolveCertificate(String certificateJson) throws Exception {
JSONObject jsonObject = JSONObject.parseObject(certificateJson);
JSONObject resource = jsonObject.getJSONObject("encrypt_certificate");
String associated_data = resource.getString("associated_data");
String nonce = resource.getString("nonce");
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes());
String decrypt = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), resource.getString("ciphertext"));
return decrypt;
}
public String getCertificatesList(String token) throws Exception {
String schema = "WECHATPAY2-SHA256-RSA2048";
HttpGet get = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
get.setHeader("Authorization", schema + " " + token);
get.setHeader("Accept", "application/json");
get.setHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse response = client.execute(get);
int statusCode = response.getStatusLine().getStatusCode();
String res = null;
if (statusCode == 200) { //处理成功
res = EntityUtils.toString(response.getEntity());
System.out.println("success,return body = " + res);
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
return res;
}
/**
* 获取签名 用于获取平台证书
*
* @throws Exception
*/
public String getAuthorizationToken() throws Exception {
AuthorizationToken token = new AuthorizationToken();
String t = token.getToken("GET", "/v3/certificates", "");
return t;
}
}
以上代码中关键性身份信息均为错误数据,请大家自行登录微信支付平台申请获取。