认证与签名
每一笔商户 API 请求都使用三个 HTTP 标头做认证:
| 标头 | 用途 |
|---|---|
X-API-Key | 商户 API key(由 Payment Gateway 开通)。 |
X-Timestamp | Unix epoch 秒数。必须在服务器时间的 ±5 分钟以内。 |
X-Signature | 对排序后参数拼接 API key 后计算的小写 hex MD5。 |
如何计算签名
- 收集所有 request body 参数成 map。
- 将 key 按 ASCII 顺序排序。
- 组成 query string
key1=value1&key2=value2,跳过值为null的字段。 - 直接拼接 API key(不加
&分隔符)。 - 对 UTF-8 bytes 计算 MD5,输出小写 hex。
参考实现
下方四份示例都已对后端 authoritative 签名实现做逐字节验证。挑你用的语言、复制、执行即可。
bash
#!/usr/bin/env bash
# MD5 signature for Payment Gateway merchant API (bash reference).
#
# Usage:
# ./sign.sh --api-key <key> --params 'k1=v1&k2=v2&...'
set -euo pipefail
API_KEY=""
PARAMS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--api-key) API_KEY="$2"; shift 2 ;;
--params) PARAMS="$2"; shift 2 ;;
*) echo "Unknown flag: $1" >&2; exit 2 ;;
esac
done
# Sort key=value pairs ASCII order (C locale), drop empty trailing field
SORTED=$(printf '%s' "$PARAMS" | tr '&' '\n' | LC_ALL=C sort | grep -v '^$' | tr '\n' '&' | sed 's/&$//')
SIGN_STRING="${SORTED}${API_KEY}"
# MD5: md5sum on Linux/CI, md5 on macOS
if command -v md5sum >/dev/null 2>&1; then
printf '%s' "$SIGN_STRING" | md5sum | cut -d' ' -f1
else
printf '%s' "$SIGN_STRING" | md5
fipython
#!/usr/bin/env python3
"""Reference implementation: MD5 signature for Payment Gateway merchant API.
Usage:
python sign.py --api-key <key> --params 'k1=v1&k2=v2&...'
Algorithm:
1. Parse params into a dict.
2. Sort keys alphabetically (ASCII order).
3. Build query string 'k=v&k=v', skipping None values.
4. Append api_key directly (no '&').
5. MD5 the UTF-8 bytes; output lowercase hex.
"""
import argparse
import hashlib
import sys
from urllib.parse import parse_qsl
def create_signature(params: dict[str, str], api_key: str) -> str:
sorted_items = sorted(params.items())
query_string = "&".join(f"{k}={v}" for k, v in sorted_items if v is not None)
return hashlib.md5((query_string + api_key).encode("utf-8")).hexdigest()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--api-key", required=True)
parser.add_argument("--params", required=True)
args = parser.parse_args()
params = dict(parse_qsl(args.params, keep_blank_values=True))
print(create_signature(params, args.api_key))
return 0
if __name__ == "__main__":
sys.exit(main())php
<?php
/**
* MD5 signature for Payment Gateway merchant API (PHP reference).
*
* Usage:
* php sign.php --api-key=<key> --params='k1=v1&k2=v2&...'
*
* Note: getopt() requires --flag=value form (no space between flag and value).
*/
$opts = getopt('', ['api-key:', 'params:']);
if (!isset($opts['api-key']) || !isset($opts['params'])) {
fwrite(STDERR, "Usage: php sign.php --api-key=<key> --params='k=v&...'\n");
exit(2);
}
parse_str($opts['params'], $params);
ksort($params, SORT_STRING);
$parts = [];
foreach ($params as $k => $v) {
if ($v === null) continue;
$parts[] = "$k=$v";
}
echo md5(implode('&', $parts) . $opts['api-key']);typescript
#!/usr/bin/env node
/**
* MD5 signature for Payment Gateway merchant API (Node.js / TypeScript reference).
*
* Usage:
* tsx sign.ts --api-key <key> --params 'k1=v1&k2=v2&...'
*/
import { createHash } from "crypto";
function parseArgs(argv: string[]): { apiKey: string; params: string } {
let apiKey = "", params = "";
for (let i = 2; i < argv.length; i++) {
if (argv[i] === "--api-key") apiKey = argv[++i];
else if (argv[i] === "--params") params = argv[++i];
}
if (!apiKey || !params) {
process.stderr.write("Usage: sign.ts --api-key <key> --params 'k=v&...'\n");
process.exit(2);
}
return { apiKey, params };
}
function createSignature(paramsQuery: string, apiKey: string): string {
const parsed = new Map<string, string>();
for (const pair of paramsQuery.split("&")) {
if (!pair) continue;
const eq = pair.indexOf("=");
const k = eq >= 0 ? pair.slice(0, eq) : pair;
const v = eq >= 0 ? pair.slice(eq + 1) : "";
parsed.set(k, v);
}
const sorted = [...parsed.entries()].sort(([a], [b]) =>
a < b ? -1 : a > b ? 1 : 0
);
const query = sorted.map(([k, v]) => `${k}=${v}`).join("&");
return createHash("md5").update(query + apiKey, "utf8").digest("hex");
}
const { apiKey, params } = parseArgs(process.argv);
process.stdout.write(createSignature(params, apiKey));签名逐步示例
给定:
- API key:
test-api-key-abc123 - 参数:
{"amount": "100.00", "currency": "THB", "merchant_order_no": "TEST001", "timestamp": "1700000000"}
逐步:
- 排序后 key:
amount、currency、merchant_order_no、timestamp - Query string:
amount=100.00¤cy=THB&merchant_order_no=TEST001×tamp=1700000000 - 拼接 API key:
amount=100.00¤cy=THB&merchant_order_no=TEST001×tamp=1700000000test-api-key-abc123 - MD5:用上方任一份示例对同一输入计算,四份输出完全相同的小写 hex digest。CI 每次 push 都会检查这份一致性。
常见陷阱
- Timestamp 漂移:服务器拒收超过 5 分钟的请求。用 NTP 同步系统时钟。
- JSON key 在 request body 内的顺序不影响 — 但用来计算签名的参数必须按 ASCII 排序,不是 locale collation。
- Null vs 空字符串:建立签名 query string 时跳过
null值;但要包含空字符串("")。 - 编码:永远用 UTF-8。签名阶段不要对 value 做 URL-encoding — 用原值签名、用 JSON body 传输。
在回调端点上验证签名
当 Payment Gateway 调用你的 callback URL 时,会用相同方式对 payload 签名。要验证,你这端用自己的 API key 重新计算一次签名,然后与 X-Signature 标头的值比对。