認證與簽章
每一筆商戶 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 標頭的值比對。