接入你的 App
請求簽名
#什麼時候需要
沒有自己後端的 App 只能把 key 放在 App 裡,只要攔截流量就能看到。給這個 key 開啟請求簽名(控制台 → API 金鑰 → 編輯 → 請求簽名)後,每個請求還必須帶一個用單獨簽名金鑰算出的簽名:只拿到 sk- key 沒用,攔截到的請求也不能重放。
#三個請求標頭
Authorization 照常傳送,再加三個請求標頭。簽名內容是時間戳記、nonce、HTTP 方法、路徑(不含查詢參數)用換行連起來;HMAC 的金鑰就是控制台裡顯示的 sig_… 字串,原樣使用。
headers
X-Timestamp: 1759312345 # unix seconds, within 5 minutes of server time
X-Nonce: 9f2c4e7a1b3d5f60 # 16-128 chars of A-Z a-z 0-9 - _, new for every request
X-Signature: hex(HMAC-SHA256(secret, "1759312345\n9f2c4e7a1b3d5f60\nPOST\n/v1/images/tasks"))#為請求簽名
sign.dart
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
const apiKey = 'sk-YOUR_KEY';
const signingSecret = 'sig_YOUR_SIGNING_SECRET';
int clockOffset = 0; // server time - device time, from X-Server-Timestamp
Map<String, String> signedHeaders(String method, String path) {
final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000 + clockOffset).toString();
final random = Random.secure();
final nonce = List.generate(16, (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0')).join();
final signature = Hmac(sha256, utf8.encode(signingSecret))
.convert(utf8.encode('$ts\n$nonce\n$method\n$path'))
.toString();
return {
'Authorization': 'Bearer $apiKey',
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
// headers: signedHeaders('POST', '/v1/images/tasks')
// GET /v1/images/tasks/{id} is signed with its own path, e.g. '/v1/images/tasks/task_123'sign.mjs
import crypto from "node:crypto";
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const signature = crypto
.createHmac("sha256", process.env.SIGNING_SECRET)
.update(`${ts}\n${nonce}\nPOST\n/v1/images/tasks`)
.digest("hex");
await fetch("https://www.aisplice.store/v1/images/tasks", {
method: "POST",
headers: {
Authorization: "Bearer sk-YOUR_KEY",
"Content-Type": "application/json",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": signature,
},
body: JSON.stringify(payload),
});#錯誤與規則
- 被拒絕的請求回傳 401,error.code 為 signature_required、signature_expired、signature_invalid 或 signature_replayed。
- signature_expired 同時回傳 X-Server-Timestamp。手機時間不準時,記下兩者差值,之後每個時間戳記都加上它,並用新的 nonce 重試一次。
- 重試也要用新的 nonce 重新簽名;用過的 nonce 會記 10 分鐘。
- 在控制台更換金鑰後,舊金鑰 30 秒內失效。關閉再開啟簽名,金鑰不變。
- 簽名防的是攔截流量,防不住有人拆解 App。建置時請做混淆(Flutter:--obfuscate),並給 key 設額度上限。
還是沒解決?
把請求 ID 或錯誤訊息傳給我們,我們來查。