Skip to content

认证与签名

每一笔商户 API 请求都使用三个 HTTP 标头做认证:

标头用途
X-API-Key商户 API key(由 Payment Gateway 开通)。
X-TimestampUnix epoch 秒数。必须在服务器时间的 ±5 分钟以内。
X-Signature对排序后参数拼接 API key 后计算的小写 hex MD5。

如何计算签名

  1. 收集所有 request body 参数成 map。
  2. 将 key 按 ASCII 顺序排序。
  3. 组成 query string key1=value1&key2=value2,跳过值为 null 的字段。
  4. 直接拼接 API key(不加 & 分隔符)。
  5. 对 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
fi
python
#!/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"}

逐步:

  1. 排序后 keyamountcurrencymerchant_order_notimestamp
  2. Query stringamount=100.00&currency=THB&merchant_order_no=TEST001&timestamp=1700000000
  3. 拼接 API keyamount=100.00&currency=THB&merchant_order_no=TEST001&timestamp=1700000000test-api-key-abc123
  4. 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 标头的值比对。