业务实践系列(5):微信退款通知之BASE64-AES-256-ECB加解密(PKCS7Padding)

最近在做支付通道开发,需要对接微信和支付宝,银联,云闪付,各大银行等。

微信支付的开放接口对于开发者来说,相对支付宝并不那么友好,主要是有些小坑,开放文档描述的不够一致。

例如,同一个属性,返回的字段名与调起支付的字段名不一致,对开发者来说容易搞混。微信支付的 SDK 中定义为抽象的方法却使用 private 控制修饰符,导致外部不能重写。

微信退款通知

微信退款通知中的退款业务数据是一个加密信息字段 req_info,需要使用商户秘钥进行解密。

备注:官方文档没有说明的一个坑,加密信息字段req_info的 BASE64 字符串使用的是 ISO_8859_1编码,而不是通常默认的 UTF-8,否则会解密失败。

解密步骤如下

  1. 对加密串 A 做 base64 解码,得到加密串 B。
  2. 对商户 key 做md5,得到 32 位 小写 key。
  3. 用 key 对加密串 B 做 AES-256-ECB 解密(PKCS7Padding)。

加解密工具类

添加依赖

JDK 8 默认支持的是 PKCS5Padding,要支持 PKCS7Padding 需要引入对应的 JCE 包。

支付宝SDK 和 spring-cloud-start 包含了 bcprov-jdk15on 依赖。

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.62</version>
</dependency>

加解密示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.clearofchina.pay.test;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.DigestUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

/**
* @desc: BASE64-AES-256-ECB加解密
* @author: gxing
* @date: 2020/6/19
*/
public class AESUtil {

/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";

/**
* @desc: 解密
* @param: [data, key]
* @author: gxing
* @date: 2020/6/20
*/
public static String decryptData(String data, String key) throws Exception {
Base64.Decoder decoder = Base64.getDecoder();
// key md5值小写
String lowMD5 = DigestUtils.md5DigestAsHex(key.getBytes(Charset.defaultCharset())).toLowerCase();
// 密钥
SecretKeySpec secretKey = new SecretKeySpec(lowMD5.getBytes(), ALGORITHM);
// 支付PKCS7Padding
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
// BASE64解密(主意字符编码,微信的 req_info 使用的是ISO_8859_1, 不是UTF-8 )
String info = new String(decoder.decode(data), StandardCharsets.ISO_8859_1);
byte[] bytes = cipher.doFinal(info.getBytes(StandardCharsets.ISO_8859_1));
return new String(bytes, Charset.defaultCharset());
}

/**
* @desc: 加密
* @param: [data, key]
* @author: gxing
* @date: 2020/6/20
*/
public static String encryptData(String data, String key) throws Exception {
// key md5值小写
String lowMD5 = DigestUtils.md5DigestAsHex(key.getBytes(Charset.defaultCharset())).toLowerCase();
SecretKeySpec secretKey = new SecretKeySpec(lowMD5.getBytes(), ALGORITHM);
// 支付PKCS7Padding
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = cipher.doFinal(data.getBytes());
// BASE64加密
return encoder.encodeToString(bytes);
}

public static void main(String[] args) throws Exception {
String apiKey = "4567qwer53321ty444rewq87656uyt";
String data = "<root>\n" +
"<out_refund_no><![CDATA[131811191610442717309]]></out_refund_no>\n" +
"<out_trade_no><![CDATA[71106718111915575302817]]></out_trade_no>\n" +
"<refund_account><![CDATA[REFUND_SOURCE_RECHARGE_FUNDS]]></refund_account>\n" +
"<refund_fee><![CDATA[3960]]></refund_fee>\n" +
"<refund_id><![CDATA[50000408942018111907145868882]]></refund_id>\n" +
"<refund_recv_accout><![CDATA[支付用户零钱]]></refund_recv_accout>\n" +
"<refund_request_source><![CDATA[API]]></refund_request_source>\n" +
"<refund_status><![CDATA[SUCCESS]]></refund_status>\n" +
"<settlement_refund_fee><![CDATA[3960]]></settlement_refund_fee>\n" +
"<settlement_total_fee><![CDATA[3960]]></settlement_total_fee>\n" +
"<success_time><![CDATA[2018-11-19 16:24:13]]></success_time>\n" +
"<total_fee><![CDATA[3960]]></total_fee>\n" +
"<transaction_id><![CDATA[4200000215201811190261405420]]></transaction_id>\n" +
"</root>";
String encode = encryptData(data, apiKey);
String decode = decryptData(encode, apiKey);
System.out.println(decode);
}
}

相关参考

业务实践系列(5):微信退款通知之BASE64-AES-256-ECB加解密(PKCS7Padding)

http://blog.gxitsky.com/2020/06/20/Business-05-wxpay-refund-notify-AES-util/

作者

光星

发布于

2020-06-20

更新于

2023-03-07

许可协议

评论