--- url: >- https://acquirer-api-docs-v4-en.pingpongx.com/notes/zh/onlinePayment/developer/sign/index.md description: 说明 API 请求签名和验签规则,覆盖待签名字符串组装、MD5/SHA256 算法选择和常见语言示例。 --- # 签名规约 PingPongCheckout API v4通过验证签名来保证请求的真实性和数据的完整性。 *** ::: warning PingPongCheckout API v4中`accId`,`clientId`,`signType`,`version`,`bizContent`均参与签名,如果之前对接的是v2或者v3版本要升级到v4,需要按照新的签名规则进行对接。 ::: ## 签名类型 | 签名类型 | 描述 | |:-------|:-------------------------------------| | MD5 | 表示选择 MD5 算法,商户使用 Salt 对报文进行摘要签名和验签 | | SHA256 | 表示选择 SHA256算法,商户使用 Salt 对报文进行摘要签名和验签 | ## 待签名字符串组装 1. 获取所有 post 请求的内容,剔除 sign 字段; 2. 按照第一个字符的键值 ASCII 码递增排序(字母升序排序); 3. 将排序后的参数与其对应值,组合成 参数=参数值 的格式,并且把这些参数用 & 字符连接起来,然后把签名秘钥(salt)放入待签名字符串的开头 , 即signContent = {salt}key1=val2&key2=val2&key3=val3,此时获取到的为完整的待签名字符串; ::: warning 注意事项 **请避免在请求参数中传递特殊字符** 当前系统会对包含 `<` 和 `>` 的请求触发 XSS 防护拦截,导致请求被拒绝。 此外,尽管系统层面不会主动拦截以下特殊字符,但考虑到上游渠道、卡组织、银行等可能存在不同的风控规则,**强烈建议避免在业务中使用这些字符**: `\` `"` `'` `{` `}` `[` `]` `:` `,` `<` `>` `&` `/` `(` `)` `;` `=` `%` `+` `\n` `\r` 为保障交易成功率和稳定性,请确保商户系统在发起请求前,在**业务层面**对参数值进行校验和清理,避免包含上述特殊字符。 ::: ## 计算签名值 > 推荐使用SHA256签名方式,安全度高于MD5 签名过程将根据请求体中的数据生成签名值,并将其添加到请求体中。签名值是使用 salt、请求参数和签名方法计算出来的字符串,取决于具体的签名方法(MD5 或 SHA256)。签名值将用于验证请求的来源和完整性。 ## 签名工具类 # 代码示例 ::: code-tabs @tab ☕ JAVA ```java:line-numbers 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 签名算法 */ // [!code highlight:4] public PingPongCheckoutClient(String salt, SignAlgorithm signAlgorithm) { this.salt = salt; this.signAlgorithm = signAlgorithm; } /** * 对请求内容进行签名,并将签名结果添加到请求参数中。 * * @param requestBody 请求内容 * @return 添加了签名结果的请求参数 */ // [!code highlight:5] public JSONObject signRequest(JSONObject requestBody) { String sign = getSign(salt, signAlgorithm, requestBody); // [!code focus] requestBody.put("sign", sign); // [!code focus] return requestBody; } /** * 获取请求内容的签名结果。 * * @param salt 盐值 * @param signAlgorithm 签名算法 * @param requestBody 请求内容 * @return 请求内容的签名结果 */ public static String getSign(String salt, SignAlgorithm signAlgorithm, JSONObject requestBody) { StringBuilder stringBuilder = new StringBuilder(); List 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; // [!code highlight:7] if (signAlgorithm == SignAlgorithm.MD5) { sign = md5Sign(salt, needSignStr); // [!code focus] } else if (signAlgorithm == SignAlgorithm.SHA256) { sign = sha256(salt, needSignStr); // [!code focus] } else { throw new IllegalArgumentException("Signature algorithm not supported"); // [!code error] } return sign; // [!code focus] } /** * 对请求内容进行MD5签名。 * * @param salt 盐值 * @param content 请求内容 * @return 请求内容的MD5签名结果 */ // [!code highlight:11] public static String md5Sign(String salt, String content) { try { MessageDigest md = MessageDigest.getInstance("MD5"); // [!code focus] md.update(salt.getBytes()); // [!code focus] md.update(content.getBytes()); // [!code focus] byte[] digest = md.digest(); return byteToHexString(digest); // [!code focus] } catch (Exception e) { throw new RuntimeException("md5签名失败", e); // [!code error] } } /** * 将字节数组转化为十六进制字符串。 * * @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签名结果 */ // [!code highlight:7] public static String sha256(String salt, String content) { try { String contentStr = salt.concat(content); // [!code focus] return DigestUtils.sha256Hex(contentStr.getBytes("UTF-8")).toUpperCase(); // [!code focus] } catch (Exception e) { throw new RuntimeException("sha256签名失败", e); // [!code error] } } /** * 签名算法枚举类。 */ public enum SignAlgorithm { MD5, SHA256 } } ``` @tab 🐘 PHP ```php:line-numbers validate($requestBody); //拼接请求参数 $signContent = $this->signContent($salt, $requestBody); // [!code focus] $sign = $this->getSign($signContent, $requestBody); // [!code focus] $requestBody['sign'] = strtoupper($sign); // [!code focus] 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 */ // [!code highlight:4] public function md5Sign(string $signContent) { return md5($signContent); // [!code focus] } /** * sha256 加密 * * @param string $signContent * @return string */ // [!code highlight:4] public function sha256(string $signContent) { return hash("sha256", $signContent); // [!code focus] } /** * 对于参与的签名参数进行校验 * * @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; } } ``` ::: ## 验签工具类 ```php:line-numbers title="Service/PingPongCheckoutDecrypt.php" signContent($salt, $requestBody); // [!code focus] $sign = $this->getSign($signData['signContent'], $signData['data']); // [!code focus] if ($signData['sign'] == strtoupper($sign)) { // [!code focus] return true; } else { return false; } } /** * 拼接请求参数 * * @param string $salt * @param array $requestBody * @return array|string */ // [!code highlight:26] 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); // [!code focus] 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); // [!code error] } $sign = $data['sign']; unset($data['sign']); // [!code focus] //根据键进行排序 ksort($data); // [!code focus] $signContent = urldecode(http_build_query($data)); $signContent = $salt . $signContent; // [!code focus] 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); } } ```