支付API的数字签名

数字签名在支付API里是保证数据完整性与交易安全的关键技术。下面为你详细介绍它的实现原理和流程:

1. 数字签名的作用

  • 验证身份:能确认请求确实是由指定的商户发出的。
  • 保证完整性:防止数据在传输途中被恶意篡改。
  • 防止重放攻击:有效避免攻击者重复使用之前的交易请求。

2. 基本实现流程

(1)准备密钥

商户和支付平台会预先协商好一对密钥,分别是AppID(用于标识商户身份)和API密钥(这是一个字符串,需要严格保密)。

(2)生成待签名数据

把请求参数按照一定规则排序并拼接成字符串。以Python代码为例:

(3)添加API密钥

将API密钥追加到待签名数据的末尾,这是为了引入只有双方知道的秘密信息。

(4)计算签名值

采用特定的哈希算法(像MD5、SHA-256等)对处理后的字符串进行加密,并将结果转换为大写形式。

(5)发送请求

把包含签名的参数通过HTTPS协议发送给支付平台。

(6)验证签名

支付平台接收到请求后,会使用相同的规则重新计算签名,然后与请求中的签名进行比对。若两者一致,就说明数据是完整且可信的。

3. 安全增强措施

  • 时间戳验证:检查timestamp参数,防止重放攻击,一般会设置5分钟的有效期。
  • 随机数(Nonce):每次请求都生成唯一的随机字符串,进一步防止重放攻击。
  • HTTPS协议:确保数据在传输过程中的安全性。

4. 注意事项

  • 密钥安全:API密钥必须严格保密,避免硬编码在客户端代码中。
  • 参数编码:所有参数都要使用UTF-8编码,防止出现中文乱码问题。
  • 签名类型:要和支付平台约定好使用的哈希算法,如MD5、SHA-256等。
  • 空值处理:空参数(如None、空字符串)不应参与签名计算。

5. 实际应用示例

下面是Java实现支付API数字签名的示例代码,包含签名生成和验证的完整流程:

5.1. 签名工具类

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

public class SignUtil {

/**
* 生成随机字符串
*/
public static String generateNonceStr(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}

/**
* 生成待签名的字符串(参数排序后拼接)
*/
public static String generateSignString(Map<String, String> params, List<String> excludeKeys) {
// 过滤空值和排除的参数
Map<String, String> filteredParams = new TreeMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value != null && !value.isEmpty() && (excludeKeys == null || !excludeKeys.contains(key))) {
filteredParams.put(key, value);
}
}

// 按字典序排序并拼接
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : filteredParams.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1); // 移除最后一个&
}
return sb.toString();
}

/**
* 生成签名
*/
public static String generateSign(String signString, String apiKey, String signType) throws Exception {
String signStringWithKey = signString + "&key=" + apiKey;

if ("MD5".equalsIgnoreCase(signType)) {
return md5(signStringWithKey);
} else if ("SHA256".equalsIgnoreCase(signType)) {
return sha256(signStringWithKey);
} else {
throw new IllegalArgumentException("不支持的签名类型: " + signType);
}
}

/**
* MD5加密
*/
private static String md5(String data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(data.getBytes());
return bytesToHexString(digest).toUpperCase();
}

/**
* SHA256加密
*/
private static String sha256(String data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data.getBytes());
return bytesToHexString(digest).toUpperCase();
}

/**
* 字节数组转十六进制字符串
*/
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = String.format("%02x", b);
sb.append(hex);
}
return sb.toString();
}

/**
* 验证签名
*/
public static boolean verifySign(Map<String, String> params, String apiKey, String signType) throws Exception {
// 提取请求中的签名
String requestSign = params.get("sign");
if (requestSign == null || requestSign.isEmpty()) {
return false;
}

// 复制参数并移除签名
Map<String, String> paramsWithoutSign = new HashMap<>(params);
paramsWithoutSign.remove("sign");

// 生成待签名的字符串
String signString = generateSignString(paramsWithoutSign, null);

// 生成签名
String expectedSign = generateSign(signString, apiKey, signType);

// 比较签名
return requestSign.equals(expectedSign);
}

/**
* 验证时间戳有效性(默认5分钟)
*/
public static boolean isValidTimestamp(String timestampStr, long expiresInSeconds) {
try {
// 注意:这里需要根据实际时间戳格式解析
// 示例假设时间戳格式为 "yyyy-MM-dd HH:mm:ss"
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date timestamp = sdf.parse(timestampStr);
long currentTime = System.currentTimeMillis();
long timestampMillis = timestamp.getTime();
long diff = (currentTime - timestampMillis) / 1000; // 转换为秒
return Math.abs(diff) <= expiresInSeconds;
} catch (Exception e) {
return false;
}
}
}

5.2. 商户端生成支付请求

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
import java.util.*;

public class MerchantClient {

private static final String APP_ID = "your_app_id";
private static final String API_KEY = "your_api_secret_key";

public static void main(String[] args) {
try {
// 创建支付请求
Map<String, String> requestParams = createPaymentRequest();

// 打印请求参数
System.out.println("生成的支付请求参数:");
for (Map.Entry<String, String> entry : requestParams.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}

// 模拟发送请求到支付平台...

} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 创建支付请求
*/
public static Map<String, String> createPaymentRequest() throws Exception {
// 准备请求参数
Map<String, String> params = new HashMap<>();
params.put("app_id", APP_ID);
params.put("order_no", "ORD" + System.currentTimeMillis());
params.put("amount", "199.99");
params.put("currency", "CNY");
params.put("timestamp", getCurrentTimeString());
params.put("nonce_str", SignUtil.generateNonceStr(32));
params.put("product_name", "测试商品");
params.put("notify_url", "https://your-server.com/notify");

// 生成签名
String signString = SignUtil.generateSignString(params, null);
String sign = SignUtil.generateSign(signString, API_KEY, "MD5");

// 添加签名到参数中
params.put("sign", sign);

return params;
}

/**
* 获取当前时间字符串(格式:yyyy-MM-dd HH:mm:ss)
*/
private static String getCurrentTimeString() {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date());
}
}

5.3. 支付平台验证请求

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
93
94
95
import java.util.*;

public class PaymentPlatform {

private static final String API_KEY = "your_api_secret_key"; // 商户注册时分配的密钥

public static void main(String[] args) {
try {
// 模拟接收到商户的支付请求
Map<String, String> requestParams = simulatePaymentRequest();

// 处理支付请求
Map<String, Object> result = processPaymentRequest(requestParams);

// 打印处理结果
System.out.println("处理结果:");
for (Map.Entry<String, Object> entry : result.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}

} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 处理支付请求
*/
public static Map<String, Object> processPaymentRequest(Map<String, String> params) {
Map<String, Object> result = new HashMap<>();

// 1. 验证时间戳
String timestamp = params.get("timestamp");
if (timestamp == null || !SignUtil.isValidTimestamp(timestamp, 300)) { // 5分钟有效期
result.put("code", 4001);
result.put("message", "请求已过期");
return result;
}

// 2. 验证签名
try {
boolean isValidSign = SignUtil.verifySign(params, API_KEY, "MD5");
if (!isValidSign) {
result.put("code", 4002);
result.put("message", "签名验证失败");
return result;
}
} catch (Exception e) {
result.put("code", 5000);
result.put("message", "签名验证异常: " + e.getMessage());
return result;
}

// 3. 签名验证通过,处理业务逻辑
result.put("code", 200);
result.put("message", "处理成功");
// 这里可以添加订单处理、金额校验等业务逻辑

return result;
}

/**
* 模拟接收到的支付请求(实际应用中从HTTP请求获取)
*/
private static Map<String, String> simulatePaymentRequest() throws Exception {
// 实际应用中,这里应该从HTTP请求中获取参数
// 这里为了演示,直接创建一个模拟的请求参数
Map<String, String> params = new HashMap<>();
params.put("app_id", "your_app_id");
params.put("order_no", "ORD" + System.currentTimeMillis());
params.put("amount", "199.99");
params.put("currency", "CNY");
params.put("timestamp", getCurrentTimeString());
params.put("nonce_str", SignUtil.generateNonceStr(32));
params.put("product_name", "测试商品");
params.put("notify_url", "https://your-server.com/notify");

// 生成签名
String signString = SignUtil.generateSignString(params, null);
String sign = SignUtil.generateSign(signString, API_KEY, "MD5");

// 添加签名到参数中
params.put("sign", sign);

return params;
}

/**
* 获取当前时间字符串(格式:yyyy-MM-dd HH:mm:ss)
*/
private static String getCurrentTimeString() {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date());
}
}

运行结果

1
2
3
处理结果:
code = 200
message = 处理成功

5.4. 注意事项

  1. 密钥安全:API密钥不应该硬编码在代码中,建议从配置文件或环境变量读取
  2. 时间戳验证:根据实际需求调整有效期(示例中为5分钟)
  3. 字符编码:确保所有参数使用UTF-8编码
  4. 异常处理:实际应用中需要完善异常处理逻辑
  5. 签名类型:根据支付平台要求选择MD5、SHA256等算法

以上代码展示了支付API数字签名的基本实现流程,在实际应用中,你需要根据具体的支付平台API文档调整参数和签名规则。