签名
在支付过程中,签名通常应用于交易请求和响应。商家使用秘钥对交易数据进行组装签名。这确保了支付信息的安全和可靠性。
签名方式:sha256
签名过程
获取商户秘钥 :登入Onerway后台 >> 点击"账户中心" >> "账户信息" >> 获取页面上 Secret key 的值
需要签名的数据:所有非空请求参数(
sign字段本身除外),根据参数名称的ASCII码升序排序,以value1value2value3...的方式拼接值,再在末尾追加商户密钥;将字符串进行
sha256签名 ;
签名/验签统一规则
- 请求签名和 webhook 验签使用同一算法:
ASCII 排序 -> 拼接 value -> 追加 secretKey -> SHA-256。 - 请求签名可按默认规则处理:仅剔除
sign,其余非空字段都参与签名(即REQUEST_EXCLUDED_KEYS = ['sign'])。 - webhook 验签场景必须按签名列为
No的字段动态剔除,不要硬编码Yes白名单。 originTransactionId按”默认参与”理解即可;只有在 webhook 验签时属于剔除字段。
webhook 不参与验签字段(当前)
'originTransactionId', 'originMerchantTxnId', 'customsDeclarationAmount', 'customsDeclarationCurrency', 'paymentMethod', 'walletTypeName', 'periodValue', 'tokenExpireTime', 'sign'
签名示例
json
// 未排序的参数
{
"test3": "test3value",
"test1": "0",
"test5": "",
"test4": "test4value",
"test6": null
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
1. 剔除空值字段及 sign 字段
即剔除 test5(空字符串)和 test6(null):
text
{
"test1": "0",
"test3": "test3value",
"test5": "",
"test4": "test4value",
"test6": null
}1
2
3
4
5
6
7
2
3
4
5
6
7
2. 将参数字段升序排序
text
{
"test1": "0",
"test3": "test3value",
"test4": "test4value",
"test5": "",
"test6": null
}1
2
3
4
5
6
7
2
3
4
5
6
7
3. 然后将值拼接成字符串
0test3valuetest4value
4. 在拼接字符串的末尾加上商户秘钥
商户密钥: 3b5e10b65bff4172a5b9ca2d2ec00a6e
拼接后的字符串: 0test3valuetest4value3b5e10b65bff4172a5b9ca2d2ec00a6e
5. 最后进行sha256签名
836831ae68fce5e61d4a64363bb72c636efe6e94b62ecaa8d1c95fc58cc9cbed
加签代码示例
签名注意事项
- 进行签名时,所有key对应的value都应该是字符串。
- 如果value是
object,请先将object转换成字符串。参考下方billingInformation、shippingInformation字段 - 如果value是
object且object嵌套有object,请先将内层object转换成字符串,再转换外层object。参考下方txnOrderMsg字段,其中products字段的值是一个由数组转换成字符串再赋值的字符串
以收银台支付请求参数为例:
json
{
"billingInformation": "{\"country\":\"DE\",\"email\":\"abel.wang@onerway.com\",\"firstName\":\"şş\",\"lastName\":\"café\",\"phone\":\"17700492982\",\"address\":\"Apt. 870\",\"city\":\"Akşehir\",\"postalCode\":\"66977\",\"identityNumber\":\"12345678\",\"province\":\"Akşehir\"}",
"merchantCustId": "1723097638000",
"merchantNo": "800209",
"merchantTxnId": "1723097638000",
"merchantTxnTime": "2024-08-08 14:13:58",
"merchantTxnTimeZone": "+08:00",
"orderAmount": "100",
"orderCurrency": "USD",
"productType": "CARD",
"shippingInformation": "{\"country\":\"DE\",\"email\":\"abel.wang@onerway.com\",\"firstName\":\"şş\",\"lastName\":\"café\",\"phone\":\"17700492982\",\"address\":\"Apt. 870\",\"city\":\"Akşehir\",\"postalCode\":\"66977\",\"identityNumber\":\"12345678\",\"province\":\"Akşehir\"}",
"sign": "d23532edf2d8d4c6cb21b622bfbef4f067ef9ac2796e89117578037d11b04e98",
"subProductType": "DIRECT",
"txnOrderMsg": "{\"products\":\"[{\\\"price\\\":\\\"110.00\\\",\\\"num\\\":\\\"1\\\",\\\"name\\\":\\\"iphone11\\\",\\\"currency\\\":\\\"USD\\\"}]\",\"returnUrl\":\"https://docs.onerway.com/\",\"transactionIp\":\"127.0.0.1\",\"appId\":\"1739545982264549376\",\"javaEnabled\":false,\"colorDepth\":\"24\",\"screenHeight\":\"1080\",\"screenWidth\":\"1920\",\"timeZoneOffset\":\"-480\",\"accept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\",\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\",\"contentLength\":\"340\",\"language\":\"zh-CN\"}",
"txnType": "SALE"
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
php
<?php
const REQUEST_EXCLUDED_KEYS = ['sign'];
function buildCanonicalString(array $payload, array $excludedKeys): string
{
ksort($payload, SORT_STRING);
$canonicalString = '';
foreach ($payload as $key => $value) {
if (in_array($key, $excludedKeys, true)) {
continue;
}
if ($value === null || $value === '') {
continue;
}
$canonicalString .= (string) $value;
}
return $canonicalString;
}
function generateSignature(
array $payload,
string $secretKey,
array $excludedKeys
): string {
$canonicalString = buildCanonicalString($payload, $excludedKeys);
return hash('sha256', $canonicalString . $secretKey);
}
$requestPayload = [
'merchantNo' => '800209',
'originTransactionId' => '1925119178365603842',
'refundAmount' => '45',
'sign' => ''
];
$secretKey = 'your-secret-key';
$signature = generateSignature($requestPayload, $secretKey, REQUEST_EXCLUDED_KEYS);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
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
java
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
@Slf4j
public class HashUtil {
private static final Set<String> REQUEST_EXCLUDED_KEYS = Set.of("sign");
public static void main(String[] args) {
TreeMap<String, Object> payload = new TreeMap<>();
payload.put("merchantNo", "800209");
payload.put("originTransactionId", "1925119178365603842");
payload.put("refundAmount", "45");
payload.put("sign", "");
// TODO: 改成自己的商户密钥
String secretKey = "你的商户密钥";
String signature = generateSignature(payload, secretKey, REQUEST_EXCLUDED_KEYS);
System.out.println("签名 = " + signature);
}
/**
* 剔除空值和 excludedKeys 字段,按 ASCII 排序后拼接 value
*
* @param payload 请求或通知参数
* @param excludedKeys 需要剔除的字段
* @return 拼接的字符串
*/
public static String buildCanonicalString(
Map<String, Object> payload,
Set<String> excludedKeys
) {
TreeMap<String, Object> sortedPayload = new TreeMap<>(payload);
StringBuilder canonicalString = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedPayload.entrySet()) {
if (excludedKeys.contains(entry.getKey())) {
continue;
}
Object value = entry.getValue();
if (value == null || "".equals(value)) {
continue;
}
canonicalString.append(value);
}
return canonicalString.toString();
}
public static String generateSignature(
Map<String, Object> payload,
String secretKey,
Set<String> excludedKeys
) {
String canonicalString = buildCanonicalString(payload, excludedKeys);
String dataToSign = canonicalString + secretKey;
final String algorithm = "SHA-256";
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
md.update(dataToSign.getBytes(StandardCharsets.UTF_8));
return byte2Hex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
/**
* 将字节数组转换为十六进制字符串表示形式。
*
* @param bytes 需要转换的字节数组
* @return 字节数组的十六进制字符串表示形式
*/
public static String byte2Hex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte value : bytes) {
String hexValue = Integer.toHexString(value & 0xFF);
if (hexValue.length() == 1) {
sb.append("0");
}
sb.append(hexValue);
}
return sb.toString();
}
}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
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
python
import hashlib
REQUEST_EXCLUDED_KEYS = {"sign"}
def build_canonical_string(payload: dict, excluded_keys: set[str]) -> str:
canonical_parts = []
for key in sorted(payload.keys()):
if key in excluded_keys:
continue
value = payload[key]
if value is None or value == "":
continue
canonical_parts.append(str(value))
return "".join(canonical_parts)
def generate_signature(payload: dict, secret_key: str, excluded_keys: set[str]) -> str:
canonical_string = build_canonical_string(payload, excluded_keys)
data_to_sign = f"{canonical_string}{secret_key}"
return hashlib.sha256(data_to_sign.encode("utf-8")).hexdigest()
if __name__ == "__main__":
payload = {
"merchantNo": "800209",
"originTransactionId": "1925119178365603842",
"refundAmount": "45",
"sign": "",
}
secret_key = "your-secret-key"
signature = generate_signature(payload, secret_key, REQUEST_EXCLUDED_KEYS)
print("签名 =", signature)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
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