数字签名在支付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); } }
private static String md5(String data) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(data.getBytes()); return bytesToHexString(digest).toUpperCase(); }
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); }
public static boolean isValidTimestamp(String timestampStr, long expiresInSeconds) { try { 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; }
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<>(); String timestamp = params.get("timestamp"); if (timestamp == null || !SignUtil.isValidTimestamp(timestamp, 300)) { result.put("code", 4001); result.put("message", "请求已过期"); return result; } 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; } result.put("code", 200); result.put("message", "处理成功"); return result; }
private static Map<String, String> simulatePaymentRequest() throws Exception { 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; }
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. 注意事项
- 密钥安全:API密钥不应该硬编码在代码中,建议从配置文件或环境变量读取
- 时间戳验证:根据实际需求调整有效期(示例中为5分钟)
- 字符编码:确保所有参数使用UTF-8编码
- 异常处理:实际应用中需要完善异常处理逻辑
- 签名类型:根据支付平台要求选择MD5、SHA256等算法
以上代码展示了支付API数字签名的基本实现流程,在实际应用中,你需要根据具体的支付平台API文档调整参数和签名规则。