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 標頭的值比對。