驗證與簽章

四個必要標頭、canonical string 組成,以及可核對的簽章範例

View as Markdown

每個 /v1 請求都必須帶上四個標頭:X-Client-IdX-TimestampX-NonceX-Signature。伺服器會用這四個值加上請求本身的內容重新算出一次簽章,與 X-Signature 比對,只有相符才會放行。

四個標頭

標頭說明
X-Client-IdOrderUp 配發的用戶端識別碼,開通時提供。
X-Timestamp送出請求當下的 Unix 秒數(不是毫秒)。與伺服器時間的誤差必須在 ±300 秒內,超過會被視為簽章失敗。
X-Nonce本次請求專用的隨機字串,例如不帶連字號的 UUID。伺服器會記住用過的 nonce,同一個 nonce 在有效期間內重複出現會被視為重放攻擊而拒絕,所以每次請求都要換一個新的值,不能固定或重複使用。
X-Signatureclient_secret 對 canonical string 做 HMAC-SHA256 後,再轉成 Base64 的結果。

canonical string 的組成

canonical string 是簽章真正計算的對象,由五行組成,行與行之間用換行字元(\n)相接:

{HTTP 方法}
{路徑,含 query string}
x-timestamp:{X-Timestamp 的值}
x-nonce:{X-Nonce 的值}
body-sha256:{request body 的 SHA-256,十六進位小寫}

五行的順序固定,欄位名稱(x-timestamp:x-nonce:body-sha256:)也要照抄,這不是欄位說明文字,是簽章輸入的一部分。

path 一定含 query string,而且逐字不正規化

第二行的「路徑」不是只有 /v1/customers 這種乾淨路徑——只要請求帶了 query string,就要把 ? 之後的部分原封不動接在路徑後面一起簽。伺服器驗證時是把收到的路徑與 query string原樣串起來計算,不會排序參數,也不會做任何正規化:你送出什麼樣的位元組,就要簽那個樣子的位元組,順序、大小寫、百分號編碼都必須與實際送出的請求完全一致。

舉例,一個查詢客戶清單的請求:

GET /v1/customers?page=1&updatedSince=2026-07-01T00%3A00%3A00Z

canonical string 的第二行必須是:

/v1/customers?page=1&updatedSince=2026-07-01T00%3A00%3A00Z

要簽的是帶著 %3A 的原始字串,不是解碼後的 /v1/customers?page=1&updatedSince=2026-07-01T00:00:00Z——把 %3A 解成 : 再簽,算出來的簽章會跟伺服器重算的不一樣,直接收到 401。同理,如果你的程式在送出前把參數重新排序(例如把 updatedSince 排到 page 前面),就必須簽「重新排序後、實際送出的那個字串」,而不是原本想像中的順序——一切以「這個 HTTP 請求實際傳輸的那一行」為準。

沒有 query string 的請求(例如 POST /v1/customers)不受影響,第二行就是乾淨的路徑本身,這也是下面 fixture 範例採用的情況。

沒有 body 時,仍要算空字串的雜湊

GET 請求或任何沒有 body 的請求,第五行不能省略,也不能留空——要對「長度為 0 的空位元組序列」算 SHA-256,而不是對空字串本身做特殊處理跳過這一步。空位元組序列的 SHA-256 固定是:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

所以一個沒有 body 的 GET /v1/product-categories 請求,canonical string 的最後一行會是 body-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

逐字核對的簽章範例

下面這組數字是用正式簽章邏輯實際跑出來的固定輸入與輸出,用來讓你核對自己那端算出來的 canonical string 與簽章是否正確。直接照抄這些值來驗證你的實作,不要自己重新湊一組。

{
"method": "POST",
"path": "/v1/customers",
"timestampUnix": 1757400000,
"nonce": "3f7a9e2c8b414d6a9c2e1a5f6d8b3c07",
"secret": "example_client_secret_do_not_use_in_production",
"body": "{\"outerSysCode\":\"ERP-C-0001\",\"name\":\"測試客戶股份有限公司\"}"
}

secret 一欄是這組範例專用的測試值,本身就標明「請勿用於正式環境」——正式的 client_secret 由 OrderUp 在開通時提供,不會是這個值。

簽章函式範例

三段範例實作的是同一套演算法:先算 body 的 SHA-256,組出五行 canonical string,再用 client_secret 對它做 HMAC-SHA256 並轉成 Base64。用上面的固定輸入分別代入,應該都能算出同一個 X-Signature

const crypto = require('node:crypto');
function buildCanonicalString({ method, pathAndQuery, timestampUnix, nonce, body }) {
const bodyBytes = Buffer.isBuffer(body) ? body : Buffer.from(body ?? '', 'utf8');
const bodySha256Hex = crypto.createHash('sha256').update(bodyBytes).digest('hex');
return [
method,
pathAndQuery,
`x-timestamp:${timestampUnix}`,
`x-nonce:${nonce}`,
`body-sha256:${bodySha256Hex}`,
].join('\n');
}
function signRequest(secret, request) {
const canonicalString = buildCanonicalString(request);
return crypto
.createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(Buffer.from(canonicalString, 'utf8'))
.digest('base64');
}
// signRequest('example_client_secret_do_not_use_in_production', {
// method: 'POST',
// pathAndQuery: '/v1/customers',
// timestampUnix: 1757400000,
// nonce: '3f7a9e2c8b414d6a9c2e1a5f6d8b3c07',
// body: '{"outerSysCode":"ERP-C-0001","name":"測試客戶股份有限公司"}',
// });
// => "tZ0/zywCPA9WgMwfhueS33FSNPnfPDHmujjwIWn7aTw="

沒有 body 的請求(例如 GET),把上面函式的 body 參數傳入長度為 0 的空位元組(Node 的 Buffer.alloc(0)、C# 的 Array.Empty<byte>()、Python 的 b"")即可,不需要另外分支處理。

收到 401 時的排查清單

401 代表這個請求沒有通過驗證,回應本身不會說明是哪一項失敗(避免洩漏線索給攻擊者)。失敗的原因不只簽章一種——四個標頭少帶或帶了空值、X-Timestamp 不是純數字、X-Client-Id 不是 OrderUp 配發的值或尚未開通,得到的都是一模一樣的 401。依序檢查:

  1. 四個標頭沒有全部帶齊,或其中一個是空值。 X-Client-IdX-TimestampX-NonceX-Signature 缺任何一個、或值是空字串,都會在簽章比對之前就被擋下。另外 X-Timestamp 必須是 Unix 秒數的純數字,寫成 ISO-8601 時間字串或帶小數點的毫秒值一樣會被拒絕。
  2. X-Client-Id 打錯了,或這組憑證還沒開通。 這個值必須與 OrderUp 配發給你的完全一致(大小寫也算),而且該組憑證必須已經是啟用狀態。剛拿到憑證、還在等開通就先接上去測,看到的就是這個 401——若其他項都排除了,請先向 OrderUp 確認這組 X-Client-Id 已經開通。
  3. 本機時鐘沒有對齊 NTP。 X-Timestamp 與伺服器時間誤差超過 ±300 秒就會被拒絕,機器上的時間飄移是最常見的原因。
  4. X-Nonce 被重複使用。 每次請求都要換一個新的隨機值;重試同一個請求時如果沿用了舊的 nonce 而不是重新產生,會被視為重放攻擊。
  5. 簽的是正規化過的 query string,不是實際送出的那個。 常見於 HTTP 用戶端函式庫會自動幫你排序參數或把 %3A 解碼成 :——簽章必須用「線路上實際傳輸的那個路徑+query string」,見上面「path 一定含 query string」的說明。
  6. body 在簽完名之後又被序列化器動過。 例如簽章時用了一份 JSON 字串,送出時卻讓 HTTP 函式庫重新序列化物件(欄位順序、空白、跳脫字元都可能因此改變),這樣簽章對應的 body 就跟實際送出的 body 不是同一組位元組了。務必確保「拿去算 body-sha256 的那份 body」與「實際放進請求裡的那份 body」逐位元組相同。
  7. client_secret 前後夾帶了空白或換行。 從設定檔或環境變數讀取時很容易多帶一個換行字元,導致 HMAC 用的 key 跟伺服器那端不一致。

排除以上七項仍然失敗,請聯繫 OrderUp,並提供發生問題那次請求的時間戳記與 nonce(請勿提供 client_secret),協助排查。