签名规约
# 签名规约
PingPongCheckout API v4通过验证签名来保证请求的真实性和数据的完整性。
注意
PingPongCheckout API v4中accId,clientId,signType,version,bizContent均参与签名,如果之前对接的是v2或者v3版本要升级到v4,需要按照新的签名规则进行对接。
# 签名类型
| 签名类型 | 描述 |
|---|---|
| MD5 | 表示选择 MD5 算法,商户使用 Salt 对报文进行摘要签名和验签 |
| SHA256 | 表示选择 SHA256算法,商户使用 Salt 对报文进行摘要签名和验签 |
# 待签名字符串组装
- 获取所有 post 请求的内容,剔除 sign 字段;
- 按照第一个字符的键值 ASCII 码递增排序(字母升序排序);
- 将排序后的参数与其对应值,组合成 参数=参数值 的格式,并且把这些参数用 & 字符连接起来,然后把签名秘钥(salt)放入待签名字符串的开头 , 即signContent = {salt}key1=val2&key2=val2&key3=val3,此时获取到的为完整的待签名字符串;
# 计算签名值
推荐使用SHA256签名方式,安全度高于MD5
签名过程将根据请求体中的数据生成签名值,并将其添加到请求体中。签名值是使用 salt、请求参数和签名方法计算出来的字符串,取决于具体的签名方法(MD5 或 SHA256)。签名值将用于验证请求的来源和完整性。
# 签名工具类
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 这个类用于对请求内容进行签名,以确保请求的安全性。
*/
public class PingPongCheckoutClient {
private final String salt; // 盐值,用于增加签名的复杂度
private final SignAlgorithm signAlgorithm; // 签名算法枚举
/**
* 构造函数,用于初始化盐值和签名算法。
*
* @param salt 盐值
* @param signAlgorithm 签名算法
*/
public PingPongCheckoutClient(String salt, SignAlgorithm signAlgorithm) {
this.salt = salt;
this.signAlgorithm = signAlgorithm;
}
/**
* 对请求内容进行签名,并将签名结果添加到请求参数中。
*
* @param requestBody 请求内容
* @return 添加了签名结果的请求参数
*/
public JSONObject signRequest(JSONObject requestBody) {
String sign = getSign(salt, signAlgorithm, requestBody); // 获取请求内容的签名
requestBody.put("sign", sign); // 将签名结果添加到请求参数中
return requestBody;
}
/**
* 获取请求内容的签名结果。
*
* @param salt 盐值
* @param signAlgorithm 签名算法
* @param requestBody 请求内容
* @return 请求内容的签名结果
*/
public static String getSign(String salt, SignAlgorithm signAlgorithm, JSONObject requestBody) {
StringBuilder stringBuilder = new StringBuilder();
List<String> keys = new ArrayList<>(requestBody.keySet());
Collections.sort(keys); // 对请求参数的键进行升序排序
for (String key : keys) {
Object valueObject = requestBody.get(key);
// 剔除空值
if (valueObject == null) {
continue;
}
// 剔除非字符串类型的值
if (!(valueObject instanceof String)) {
throw new IllegalArgumentException("request body illegal");
}
String value = (String) valueObject;
if (StringUtils.isNotBlank(value)) {
stringBuilder.append(key).append("=").append(value).append("&"); // 将请求参数的键和值拼接成字符串
}
}
String needSignStr = stringBuilder.toString();
if (needSignStr.endsWith("&")) {
needSignStr = needSignStr.substring(0, needSignStr.length() - 1); // 去掉最后一个 & 符号
}
String sign = null;
if (signAlgorithm == SignAlgorithm.MD5) {
sign = md5Sign(salt, needSignStr); // 调用 md5Sign 方法对字符串进行 MD5 加密
} else if (signAlgorithm == SignAlgorithm.SHA256) {
sign = sha256(salt, needSignStr); // 调用 sha256 方法对字符串进行 SHA256 加密
} else {
throw new IllegalArgumentException("Signature algorithm not supported"); // 如果签名算法不支持,则抛出异常
}
return sign; // 返回签名结果
}
/**
* 对请求内容进行MD5签名。
*
* @param salt 盐值
* @param content 请求内容
* @return 请求内容的MD5签名结果
*/
public static String md5Sign(String salt, String content) {
try {
MessageDigest md = MessageDigest.getInstance("MD5"); // 创建 MD5 实例
md.update(salt.getBytes()); // 将盐值加入到加密中
md.update(content.getBytes()); // 将请求内容加入到加密中
byte[] digest = md.digest(); // 获取字节数组
return byteToHexString(digest); // 将字节数组转换成十六进制字符串
} catch (Exception e) {
throw new RuntimeException("md5签名失败", e); // 如果加密过程出错,则抛出异常
}
}
/**
* 将字节数组转化为十六进制字符串。
*
* @param bytes 字节数组
* @return 十六进制字符串
*/
public static String byteToHexString(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
hexString.append(hex.toUpperCase()); // 将转换后的十六进制字符串拼接起来
}
return hexString.toString(); // 返回转换后的十六进制字符串
}
/**
* 对请求内容进行SHA256签名。
*
* @param salt 盐值
* @param content 请求内容
* @return 请求内容的SHA256签名结果
*/
public static String sha256(String salt, String content) {
try {
String contentStr = salt.concat(content); // 将盐值和请求内容拼接成一个字符串
return DigestUtils.sha256Hex(contentStr.getBytes("UTF-8")).toUpperCase(); // 调用 DigestUtils.sha256Hex 方法对字符串进行 SHA256 加密,并将加密结果转换成大写的十六进制字符串
} catch (Exception e) {
throw new RuntimeException("sha256签名失败", e); // 如果加密过程出错,则抛出异常
}
}
/**
* 签名算法枚举类。
*/
public enum SignAlgorithm {
MD5,
SHA256
}
}
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php
namespace App\Service;
/**
* 这个类用于对请求内容进行签名,以确保请求的安全性。
*/
class PingPongCheckoutClient
{
const SignAlgorithm = ['MD5', 'SHA256']; // 签名加密方式
/**
* 对请求内容进行签名,并将签名结果添加到请求参数中
*
* @param string $salt
* @param array $requestBody
*
* @return array
*/
public function signRequest(string $salt, array $requestBody)
{
//参数校验
$this->validate($requestBody);
//拼接请求参数
$signContent = $this->signContent($salt, $requestBody);
$sign = $this->getSign($signContent, $requestBody);
$requestBody['sign'] = strtoupper($sign);
if (is_array($requestBody['bizContent'])) {
$requestBody['bizContent'] = json_encode($requestBody['bizContent'], JSON_UNESCAPED_SLASHES);
}
return $requestBody;
}
/**
* 拼接请求参数
*
* @param string $salt
* @param array $requestBody
* @return array|string
*/
public function signContent(string $salt, array $requestBody)
{
unset($requestBody['sign']);
if (is_array($requestBody['bizContent'])) {
$requestBody['bizContent'] = json_encode($requestBody['bizContent'],JSON_UNESCAPED_SLASHES);
}
//根据键进行排序
ksort($requestBody);
$signContent = urldecode(http_build_query($requestBody));
$signContent = $salt . $signContent;
return $signContent;
}
/**
* 获取请求内容的签名结果。
*
* @param string $signContent
* @param array $requestBody
* @return array|string
*/
public function getSign(string $signContent, array $requestBody)
{
switch ($requestBody['signType']) {
case "MD5":
$sign = $this->md5Sign($signContent);
break;
case "SHA256":
$sign = $this->sha256($signContent);
break;
}
return $sign;
}
/**
* md5 加密
*
* @param string $signContent
* @return string
*/
public function md5Sign(string $signContent)
{
return md5($signContent);
}
/**
* sha256 加密
*
* @param string $signContent
* @return string
*/
public function sha256(string $signContent)
{
return hash("sha256", $signContent);
}
/**
* 对于参与的签名参数进行校验
*
* @param array $requestBody
* @return true
* @throws \Exception
*/
public function validate(array $requestBody)
{
$keys = ['accId', 'clientId', 'signType', 'version', 'bizContent'];
foreach ($keys as $key) {
if (!isset($requestBody[$key]) || empty($requestBody[$key])) {
throw new \Exception('invalid -' . $key);
}
}
if (!array($requestBody['signType'], self::SignAlgorithm)) {
throw new \Exception('invalid - signType' );
}
if (!is_array($requestBody['bizContent']) && !is_string($requestBody['bizContent'])) {
throw new \Exception('invalid - bizContent' );
}
return true;
}
}
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
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
// Make sure to add code blocks to your code group
# 验签工具类
<?php
namespace App\Service;
/**
* 这个类用于对请求内容进行验签,以确保请求的安全性。
*/
class PingPongCheckoutDecrypt
{
/**
* 对异步通知内容进行验签
*
* @param string $salt
* @param array $requestBody {"clientId":"2023120712300910285","code":"000000","bizContent":"{\"exchangedCurrency\":\"USD\",\"amount\":\"1.080000\",\"authenticationInfo\":{\"avsResult\":\"Unknown\",\"cvvResult\":\"Y\",\"threeDSecure\":\"N\"},\"cardInfo\":{\"firstName\":\"James\",\"isoCountryA2\":\"RU\",\"lastName\":\"LeBron\",\"lastFourDigits\":\"1112\",\"cardLevel\":\"CLASSIC\",\"paymentBrand\":\"VISA\",\"cardType\":\"CREDIT\",\"issuringBank\":\"\",\"ipCountry\":\"CN\",\"firstSixDigits\":\"401200\",\"isoCountry\":\"RUSSIAN FEDERATION\"},\"issuerInfo\":{\"issuerResultMsg\":\"Successful approval/completion or V .I.P .PIN\\n verification is successful\",\"issuerResultCode\":\"00\"},\"threeDSecure\":\"\",\"remark\":\"Remark customer defined txt\",\"transactionTime\":\"1702349526000\",\"transactionId\":\"2023121250013357\",\"notifyType\":\"RECHARGE\",\"requestId\":\"3931b66e-78ba-4107-d75d-098a005f7bc3\",\"merchantTransactionId\":\"PShop2023121210510ES\",\"paymentMethod\":{\"type\":\"VISA\"},\"currency\":\"USD\",\"exchangedAmount\":\"1.080000\",\"captureDelayHours\":0,\"status\":\"SUCCESS\"}","sign":"B9BFE38FFE24B81FCAA41FC46F5560CB","accId":"2023120712300910285520","description":"Transaction succeeded","signType":"MD5"}
*
* @return boolean
*/
public function decryptRequest(string $salt, string $requestBody)
{
//拼接请求参数
$signData = $this->signContent($salt, $requestBody);
$sign = $this->getSign($signData['signContent'], $signData['data']);
if ($signData['sign'] == strtoupper($sign)) {
return true;
} else {
return false;
}
}
/**
* 拼接请求参数
*
* @param string $salt
* @param array $requestBody
* @return array|string
*/
public function signContent(string $salt, string $requestBody)
{
//替换特殊字符
$requestBody = str_replace("\\n", '\\\\n', $requestBody);
$requestBody = str_replace("\\r", '\\\\r', $requestBody);
$requestBody = str_replace("\\f", '\\\\f', $requestBody);
$requestBody = str_replace("\\t", '\\\\t', $requestBody);
$requestBody = str_replace("\\v", '\\\\v', $requestBody);
$requestBody = str_replace('\\\\"', '\\\\\\"', $requestBody);
$data = json_decode($requestBody,true);
if (!$data) {
if (function_exists('json_last_error') && json_last_error() != 0) {
$error = json_last_error_msg();
} else {
$error = 'data does not meet the requirements';
}
throw new \Exception($error);
}
$sign = $data['sign'];
unset($data['sign']);
//根据键进行排序
ksort($data);
$signContent = urldecode(http_build_query($data));
$signContent = $salt . $signContent;
return ['sign' => $sign, 'data' => $data, 'signContent' => $signContent];
}
/**
* 获取请求内容的签名结果。
*
* @param string $signContent
* @param array $requestBody
* @return array|string
*/
public function getSign(string $signContent, array $requestBody)
{
switch ($requestBody['signType']) {
case "MD5":
$sign = $this->md5Sign($signContent);
break;
case "SHA256":
$sign = $this->sha256($signContent);
break;
}
return $sign;
}
/**
* md5 加密
*
* @param string $signContent
* @return string
*/
public function md5Sign(string $signContent)
{
return md5($signContent);
}
/**
* sha256 加密
*
* @param string $signContent
* @return string
*/
public function sha256(string $signContent)
{
return hash("sha256", $signContent);
}
}
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
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
// Make sure to add code blocks to your code group
上次更新: 2025/08/14, 14:08:19
