接入文档 V0.1.0

签名契约

中台与站点之间只有一条信任边界:HMAC-SHA256 签名。所有 /api/v1/* 接口都必须带四个请求头。

算法(可跨语言实现)

签名串 = METHOD "\n" PATH "\n" TIMESTAMP "\n" NONCE "\n" SHA256_HEX(原始请求体)
签名   = hex( HMAC_SHA256(密钥, 签名串) )
请求头含义约束
X-Hub-Site站点短码注册时定的那个
X-Hub-TimeUnix 秒与中台时钟偏差 ≤ 300 秒
X-Hub-Nonce随机串16–64 位;同一站点 15 分钟内不可重复
X-Hub-Signature签名64 位小写十六进制

四条约定,违反任一条都会 401

  1. 签名接口一律 POST,PATH 只取 URL 的路径部分,不含域名、不含 query、不含末尾多余斜杠。
    不做 query 归一化是因为各家框架对参数的排序与编码不一致,跨语言一定会踩。需要传参就放进 JSON body。
  2. 签名基于原始字节的请求体。先 json_decode 再 json_encode 一遍,字节序就变了,签名必然对不上。
  3. 密钥不进代码仓库。放环境变量或站点自己的加密配置里;两把密钥(接入 / 回调)分开使用、分开保管。
  4. 出站回调(中台 → 站点)用同一套算法,但密钥换成「回调密钥」,请求头一样是这四个。

PHP 实现(15 行,不依赖任何库)

function hub_sign(string $secret, string $method, string $path, int $ts, string $nonce, string $body): string {
    $s = strtoupper($method) . "\n" . '/' . ltrim($path, '/') . "\n" . $ts . "\n" . $nonce . "\n" . hash('sha256', $body);
    return hash_hmac('sha256', $s, $secret);
}

$body  = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$ts    = time();
$nonce = bin2hex(random_bytes(12));
$sig   = hub_sign($secret, 'POST', '/api/v1/pay/order', $ts, $nonce, $body);

$ch = curl_init('https://wx.onzx.cn/api/v1/pay/order');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 8, CURLOPT_SSL_VERIFYPEER => true,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'X-Hub-Site: ' . $siteKey, 'X-Hub-Time: ' . $ts, 'X-Hub-Nonce: ' . $nonce, 'X-Hub-Signature: ' . $sig,
  ],
]);
$r = json_decode((string)curl_exec($ch), true);
if (empty($r['ok'])) { /* 按 $r['code'] 分支:见错误码总表 */ }

响应信封

// 成功
{ "ok": true, "data": { … }, "hub": "0.1.0" }

// 失败(HTTP 4xx/5xx 同时给出)
{ "ok": false, "code": "sign_mismatch", "msg": "签名与密钥不匹配…", "hint": "…" }

按 code 分支处理,不要按 msg 文案判断 —— 文案会为可读性调整,code 不会变。

401 的排查顺序(按命中概率排)

code十有八九是这个原因
sign_mismatchbody 被重新编码过;或 path 带了域名/query;或用了另一个站点的密钥
sign_window服务器时钟不同步(先 ntpdate/chrony 对时,别改容差)
nonce_replaynonce 复用 —— 每次请求都要新的,别把整个请求对象缓存了
sign_format签名是大写、或带了 sha256= 前缀、或被 base64 了
secret_missing站点刚创建或刚重置过密钥,还没配到代码里

把「双方算出的签名串」逐字符比对是最快的定位方式:用诊断台的签名计算器 / 反向验签。

shell 版(手测用)

export WXHUB_SECRET=你的接入密钥
BODY='{}'
TIME=$(date +%s)
NONCE=$(openssl rand -hex 12)
HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -r | cut -d' ' -f1)
SIGN=$(printf '%s\n%s\n%s\n%s\n%s' 'POST' '/api/v1/ping' "$TIME" "$NONCE" "$HASH" \
       | openssl dgst -sha256 -hmac "$WXHUB_SECRET" -r | cut -d' ' -f1)
curl -sS -X POST 'https://wx.onzx.cn/api/v1/ping' \
  -H 'Content-Type: application/json' -H 'X-Hub-Site: 你的短码' \
  -H "X-Hub-Time: $TIME" -H "X-Hub-Nonce: $NONCE" -H "X-Hub-Signature: $SIGN" \
  --data "$BODY"

本页内容随代码一起版本化(V0.1.0)。发现文档与实际行为不一致,请当缺陷报告。