签名机制
为保证数据传输安全,DaxPay 支付 API 采用 RSA 非对称加密签名机制。请求、响应和异步回调均需签名验证,防止篡改。
密钥体系
DaxPay 使用 RSA 密钥对进行签名验签:
| 密钥 | 保管方 | 用途 |
|---|---|---|
| 商户私钥 | 商户服务器 | 对请求签名 |
| 商户公钥 | DaxPay 平台 | 平台验证请求签名 |
| 平台私钥 | DaxPay 平台 | 对响应/回调签名 |
| 平台公钥 | 商户服务器 | 商户验证响应/回调签名 |
密钥生成
推荐使用 2048 位 RSA 密钥对。可使用 OpenSSL 或 Java KeyPairGenerator 生成。
签名字段位置
签名相关字段在请求/响应 JSON 体内,不在 HTTP 请求头:
| 字段 | 位置 | 说明 |
|---|---|---|
| sign | JSON 体 | 签名值(Base64 编码) |
| reqId | JSON 体 | 请求唯一标识,防重放 |
| nonceStr | JSON 体 | 随机字符串,防重放 |
| reqTime | JSON 体 | 请求时间(yyyy-MM-dd HH:mm:ss,GMT+8) |
签名生成步骤
1. 构造待签名字符串
- 获取请求 JSON 中的所有字段(含嵌套对象的扁平化展开)。
- 排除
sign字段本身。 - 排除值为空的字段(null 或空字符串)。
- 将剩余字段按 key 的 ASCII 字典序 升序排列。
- 拼接为
key1=value1&key2=value2格式。
嵌套对象与列表的展开规则
- 嵌套对象使用点号连接:
terminal.terminalNo=value - 列表元素使用中括号索引:
limitPay[0]=value - 展开后整体参与排序和拼接
2. RSA 签名
使用商户私钥对待签名字符串进行签名:
- 算法:
SHA256withRSA - 编码:UTF-8
- 输出:Base64 编码字符串
3. 填入 sign 字段
将 Base64 签名结果填入请求 JSON 的 sign 字段。
响应验签
商户收到响应后,使用平台公钥验证 sign 字段:
- 获取响应 JSON 中的所有字段。
- 移除
sign字段。 - 按与请求签名相同的方式:参数升序 → 拼接。
- 使用平台公钥以
SHA256withRSA验签。
代码示例
Java — 生成签名
java
import java.security.*;
import java.util.*;
import java.util.Base64;
// 1. 构造请求参数(TreeMap 自动按 key 排序)
Map<String, String> params = new TreeMap<>();
params.put("appId", "APP001");
params.put("bizOrderNo", "ORDER20241201001");
params.put("method", "wechat_qr");
params.put("mchNo", "M200000001");
params.put("nonceStr", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS");
params.put("notifyUrl", "https://your-domain.com/notify");
params.put("amount", "100");
params.put("reqId", "REQ20241201001");
params.put("reqTime", "2024-12-01 12:00:00");
params.put("title", "测试商品");
// 2. 拼接待签名字符串(排除 sign,按 ASCII 排序)
String signStr = params.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 3. RSA 签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey); // 商户私钥
signature.update(signStr.getBytes(StandardCharsets.UTF_8));
String sign = Base64.getEncoder().encodeToString(signature.sign());Java — 验签响应/回调
java
// 1. 移除 sign 字段,按 key 排序拼接
String verifyStr = sortedParams.entrySet().stream()
.filter(e -> !"sign".equalsIgnoreCase(e.getKey()))
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 2. 使用平台公钥验签
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey); // 平台公钥
signature.update(verifyStr.getBytes(StandardCharsets.UTF_8));
byte[] signBytes = Base64.getDecoder().decode(receivedSign);
boolean valid = signature.verify(signBytes);
if (!valid) {
throw new RuntimeException("签名验证失败");
}签名规则要点
| 规则 | 说明 |
|---|---|
| 排序方式 | 按参数 key 的 ASCII 码升序 |
| 拼接格式 | key1=value1&key2=value2(无空格、无转义) |
| 空值处理 | 值为 null 或空字符串的参数不参与签名 |
| sign 排除 | sign 字段本身不参与签名计算(大小写不敏感) |
| 编码 | 签名字符串统一使用 UTF-8 |
| 签名算法 | SHA256withRSA |
| 输出格式 | Base64 编码字符串 |
| 签名位置 | JSON 请求体 sign 字段(非 HTTP Header) |