签名规约
约 2113 字大约 7 分钟
2025-03-07
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,此时获取到的为完整的待签名字符串;
注意事项
请避免在请求参数中传递特殊字符
当前系统会对包含 < 和 > 的请求触发 XSS 防护拦截,导致请求被拒绝。
此外,尽管系统层面不会主动拦截以下特殊字符,但考虑到上游渠道、卡组织、银行等可能存在不同的风控规则,强烈建议避免在业务中使用这些字符:
\ " ' { } [ ] : , < > & / ( ) ; = % + \n \r
为保障交易成功率和稳定性,请确保商户系统在发起请求前,在业务层面对参数值进行校验和清理,避免包含上述特殊字符。
计算签名值
推荐使用SHA256签名方式,安全度高于MD5
签名过程将根据请求体中的数据生成签名值,并将其添加到请求体中。签名值是使用 salt、请求参数和签名方法计算出来的字符串,取决于具体的签名方法(MD5 或 SHA256)。签名值将用于验证请求的来源和完整性。
签名工具类
代码示例
☕ JAVA
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);
} else if (signAlgorithm == SignAlgorithm.SHA256) {
sign = sha256(salt, needSignStr);
} 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");
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();
} catch (Exception e) {
throw new RuntimeException("sha256签名失败", e);
}
}
/**
* 签名算法枚举类。
*/
public enum SignAlgorithm {
MD5,
SHA256
}
}🐘 PHP
<?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 (!in_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;
}
}验签工具类
Service/PingPongCheckoutDecrypt.php
<?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);
}
}