跳到主要内容

签名

所有发送至 GLODIPAY 的请求必须包含 signature 字段。GLODIPAY 的所有响应和 IPN Payload 也包含 signature 字段,以便您验证真实性。

签名使用 RSA with MD5 (md5WithRSAEncryption)。

您的 RSA Private KeyRSA Public Key 均可在商户后台的 API Keys 页面中获取。


生成签名 (商户 → GLODIPAY)

使用您的 RSA Private Key(从商户后台获取)对请求 Payload 进行签名。

步骤:

  1. 过滤 Payload:收集除 signature 以外的所有请求参数。移除任何值为空字符串 ("")、nullundefined 的键。
  2. 序列化嵌套数据:如果字段值是对象或数组(例如 metadatabrowserDetailstransactionDocuments),请使用您所用语言的默认编码器将其转换为 JSON 字符串(不含空格)。
  3. 对顶级键排序:按自然升序对结果键进行排序(区分大小写)。在 PHP 中使用 SORT_NATURAL,在 JavaScript 中使用 localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
  4. 类型转换与修整 (Casting & Trimming)
    • 将数字转换为字符串。
    • 转换布尔值:true 变为 "1"false 变为空字符串 ""
    • 修整所有值的前导/尾随空格 (trim)。
  5. 规范化 (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 字符时,签名将不匹配。
  6. 签名 (Signing):使用 md5WithRSAEncryption 对规范化字符串进行签名。
  7. 编码 (Encoding):对二进制输出进行 Base64 编码,并将其作为 signature 字段添加。

验证签名 (GLODIPAY → 商户)

使用 RSA Public Key(在 Portal 中可获取)验证 GLODIPAY 的响应和 Webhook。

步骤:

  1. 提取 signature 值并从 Payload 中移除 signature 键。
  2. 按照上述相同的 过滤字符串化排序修整 步骤进行操作。
  3. 使用 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');
}

Bookmarks

No bookmarks yet.
Hover over a heading and click to save a section.