签名
所有发送至 GLODIPAY 的请求必须包含 signature 字段。GLODIPAY 的所有响应和 IPN Payload 也包含 signature 字段,以便您验证真实性。
签名使用 RSA with MD5 (md5WithRSAEncryption)。
您的 RSA Private Key 和 RSA Public Key 均可在商户后台的 API Keys 页面中获取。
生成签名 (商户 → GLODIPAY)
使用您的 RSA Private Key(从商户后台获取)对请求 Payload 进行签名。
步骤:
- 过滤 Payload:收集除
signature以外的所有请求参数。移除任何值为空字符串 ("")、null或undefined的键。 - 序列化嵌套数据:如果字段值是对象或数组(例如
metadata、browserDetails、transactionDocuments),请使用您所用语言的默认编码器将其转换为 JSON 字符串(不含空格)。 - 对顶级键排序:按自然升序对结果键进行排序(区分大小写)。在 PHP 中使用
SORT_NATURAL,在 JavaScript 中使用localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })。 - 类型转换与修整 (Casting & Trimming):
- 将数字转换为字符串。
- 转换布尔值:
true变为"1",false变为空字符串""。 - 修整所有值的前导/尾随空格 (trim)。
- 规范化 (Canonicalization):将排序后的对象序列化为 JSON 字符串,并将所有非 ASCII Unicode 字符转义为
\uXXXX(RFC 8259)。在 PHP 中,json_encode($data)默认执行此操作——不要使用JSON_UNESCAPED_UNICODE。在 Node.js 中,JSON.stringify()默认不转义 Unicode——请应用以下修复。- **Node.js — 正斜杠:**应用
.replace(/\//g, '\\/')以匹配 PHP 的默认行为。 - **Node.js — Unicode (RFC 8259):**应用
.replace(/[\u0080-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))来转义所有非 ASCII 字符。这是必需的,因为JSON.stringify()会将á、đ、ễ等字符保留为原始 UTF-8,而 PHP 会将它们转义为á、đ、ễ。没有此修复,当 Payload 包含非 ASCII 字符时,签名将不匹配。
- **Node.js — 正斜杠:**应用
- 签名 (Signing):使用
md5WithRSAEncryption对规范化字符串进行签名。 - 编码 (Encoding):对二进制输出进行 Base64 编码,并将其作为
signature字段添加。
验证签名 (GLODIPAY → 商户)
使用 RSA Public Key(在 Portal 中可获取)验证 GLODIPAY 的响应和 Webhook。
步骤:
- 提取
signature值并从 Payload 中移除signature键。 - 按照上述相同的 过滤、字符串化、排序 和 修整 步骤进行操作。
- 使用 GLODIPAY 公钥和
md5WithRSAEncryption对规范化字符串与提取的签名进行验证。
代码示例
PHP
<?php
function generateSignature(array $data, string $privateKeyStr): string
{
unset($data['signature']);
// 1. Filter out empty/null values
$data = array_filter($data, function($v) {
return $v !== "" && $v !== null;
});
// 2. Stringify nested objects
foreach ($data as $k => $v) {
if (is_array($v)) $data[$k] = json_encode($v);
}
// 3. Sort keys
ksort($data, SORT_NATURAL);
// 4. Casting and trimming
array_walk_recursive($data, function (&$v) {
if (is_bool($v)) $v = $v ? "1" : "";
else $v = trim((string)$v);
});
// 5. Sign (PHP json_encode escapes '/' by default)
$key = openssl_pkey_get_private($privateKeyStr);
openssl_sign(json_encode($data), $signature, $key, 'md5WithRSAEncryption');
return base64_encode($signature);
}
Node.js
// Save as script.mjs and run: node script.mjs
import { createSign } from 'crypto';
function phpCast(v) {
if (typeof v === 'number') return String(v);
if (typeof v === 'boolean') return v ? '1' : '';
if (typeof v === 'string') return v.trim();
if (Array.isArray(v)) return v.map(phpCast);
if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, val]) => [k, phpCast(val)]));
return v;
}
function generateSignature(data, privateKey) {
const sorted = {};
Object.keys(data)
.filter(k => k !== 'signature')
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }))
.forEach(k => { sorted[k] = data[k]; });
const canonical = JSON.stringify(phpCast(sorted))
.replace(/\//g, '\\/')
.replace(/[\u0080-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
const sign = createSign('md5WithRSAEncryption');
sign.update(canonical);
return sign.sign(privateKey, 'base64');
}