目錄本文共 6 節
交易所 API 簽名,就是用你的 Secret Key 對這次請求的內容計算一個 HMAC-SHA256 值,隨請求一起發給交易所,交易所用同一個 Secret 重新算一遍,結果一致才執行。幣安的簽名原文是查詢字串(加請求體),結果輸出為十六進位制,放在 signature 引數裡;歐易的簽名原文是 timestamp + method + requestPath + body,結果做 Base64 編碼後放進 OK-ACCESS-SIGN 請求頭。下面用兩家官方文件裡的示例引數,只用 Python 標準庫把簽名一步步算出來,幣安的結果與官方文件完全一致。
為什麼交易所 API 需要簽名?
API Key 只是一個公開的「帳號」,每次請求都會明文傳送。如果只憑 API Key 就能下單,任何截獲請求的人都可以冒充你。簽名解決了三個問題:
- 證明你持有 Secret:Secret 從不隨請求傳送,只用來計算簽名,別人截獲了請求也拿不到它。
- 防篡改:請求裡的任何一個字元被改動,簽名就對不上,交易所會拒絕。
- 防重放:簽名原文裡包含時間戳,過期的請求即使簽名正確也會被拒絕,舊請求無法被反覆提交。
伺服器那一端的流程可以這樣理解:收到請求後,先根據 API Key 找到對應的 Secret 和這個 Key 的權限、IP 白名單;再檢查時間戳是否在允許的時間視窗內;然後按同樣的規則把請求內容拼成簽名原文,用 Secret 算出簽名,和你發來的簽名逐字比較。任何一步不通過,請求都不會進入撮合系統。所以簽名錯誤不會產生任何訂單,只是請求被拒絕,可以放心排查。
這也是為什麼 Secret Key 和 Passphrase 一旦洩露,就必須立刻刪除 Key:拿到 Secret 的人可以自己算出任何請求的有效簽名。申請 Key 和設定權限的方法見 幣安 API Key 申請教程 和 歐易 API 建立教程。
HMAC-SHA256 是什麼?
SHA-256 是一種雜湊函式:任意長度的輸入,都會得到固定 32 位元組的輸出,輸入改一個字元,輸出就完全不同,而且無法從輸出倒推輸入。HMAC 是在雜湊的基礎上加入一個金鑰,只有知道金鑰的人才能算出相同的結果。用 Python 標準庫直觀感受一下:
import hashlib
import hmac
msg = b"hello"
print("SHA256 :", hashlib.sha256(msg).hexdigest())
print("HMAC key=a :", hmac.new(b"a", msg, hashlib.sha256).hexdigest())
print("HMAC key=b :", hmac.new(b"b", msg, hashlib.sha256).hexdigest())
print("摘要位元組數 :", len(hmac.new(b"a", msg, hashlib.sha256).digest()))
執行輸出:
SHA256 : 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
HMAC key=a : 71073df21b4bbf7b287cfb0135537dd21953934b12ec4ee1c393e26a98cd7c8c
HMAC key=b : b002a440dd7b0818b7e924189cbffad521961117cda6a71c41d6dad2d4caf4ae
摘要位元組數 : 32
同樣的訊息,金鑰不同,結果完全不同。32 位元組的結果有兩種常見寫法:hexdigest() 轉成 64 位十六進位制字串,幣安用這種;digest() 拿到原始位元組再做 Base64,得到 44 個字元,歐易用這種。兩家用的是同一個演算法,差別在於籤什麼和結果怎麼編碼。
幣安 API 簽名怎麼算?
幣安文件對 HMAC Key 的簽名規則是:
- 把所有引數按
引數名=值用&連起來,非 ASCII 字元要先做百分號編碼。 - 如果同時有查詢字串和請求體,簽名原文是兩者直接拼接,中間不加分隔符。
- 用 Secret Key 對原文做 HMAC-SHA256,結果轉為十六進位制。
- 把結果作為
signature引數附加到請求裡,API Key 放在請求頭X-MBX-APIKEY。
下面使用幣安文件中公開的示例 Secret Key 和下單引數(這是官方演示用的值,不是任何人的真實金鑰):
import hmac
import hashlib
from urllib.parse import urlencode
# 幣安官方文件中的示例 secretKey(僅用於演示,不是真實金鑰)
secret = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"
params = {
"symbol": "LTCBTC",
"side": "BUY",
"type": "LIMIT",
"timeInForce": "GTC",
"quantity": "1",
"price": "0.1",
"recvWindow": "5000",
"timestamp": "1499827319559",
}
payload = urlencode(params) # 按插入順序拼成 key=value&key=value
signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
print("payload :", payload)
print("signature:", signature)
print("與文件一致:", signature == "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71")
# 非 ASCII 交易對示例:先百分號編碼再簽名
params["symbol"] = "123456"
payload2 = urlencode(params)
sig2 = hmac.new(secret.encode(), payload2.encode(), hashlib.sha256).hexdigest()
print("payload2 :", payload2)
print("signature2:", sig2)
print("與文件一致:", sig2 == "e1353ec6b14d888f1164ae9af8228a3dbd508bc82eb867db8ab6046442f33ef3")
執行輸出:
payload : symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559
signature: c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71
與文件一致: True
payload2 : symbol=%EF%BC%91%EF%BC%92%EF%BC%93%EF%BC%94%EF%BC%95%EF%BC%96&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559
signature2: e1353ec6b14d888f1164ae9af8228a3dbd508bc82eb867db8ab6046442f33ef3
與文件一致: True
兩個示例的簽名都與幣安文件給出的結果一字不差。也可以用 OpenSSL 在命令列交叉驗證,結果相同:
echo -n "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559" \
| openssl dgst -sha256 -hmac "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"
# SHA2-256(stdin)= c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71
最後,簽名要追加在引數末尾:傳送的查詢字串就是 payload + "&signature=" + signature。引數順序本身沒有強制要求,但傳送的順序必須和簽名時完全一樣。
timestamp 和 recvWindow 是什麼規則?
截至 2026 年 10 月,幣安文件的時間規則如下:
timestamp是發出請求時的毫秒時間戳(也支援微秒),每個簽名請求都必須帶。recvWindow是請求的有效期,單位毫秒,不傳預設 5000,最大 60000,文件建議用 5000 或更小。- 伺服器的判斷邏輯是:
timestamp不能比伺服器時間快 1 秒以上,且伺服器時間減去timestamp不能超過recvWindow。
不滿足就返回 -1021。我們用文件裡 2017 年的示例時間戳去請求真實介面,幣安的返回是:
{"code":-1021,"msg":"Timestamp for this request is outside of the recvWindow."}
這次請求用的是一個格式正確但並不存在的 Key,返回的卻是 -1021 而不是 Key 無效,說明時間戳過期時會先被攔下。所以本機時鐘不準時,你看到的往往是 -1021,而不是簽名錯誤。解決辦法是開啟電腦的自動對時,或者先呼叫 GET /api/v3/time 計算本機與伺服器的時間差,生成時間戳時加以修正。
用標準庫發一個完整的幣安簽名請求
把上面的步驟串起來,就是一個完整的查帳戶餘額請求。下面預設連幣安現貨測試網,Key 從環境變數讀取:
import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.request
from urllib.parse import urlencode
API_KEY = os.environ["BINANCE_API_KEY"]
SECRET = os.environ["BINANCE_API_SECRET"].strip()
BASE = "https://testnet.binance.vision" # 測試網;實盤為 https://api.binance.com
query = urlencode({"omitZeroBalances": "true", "recvWindow": 5000, "timestamp": int(time.time() * 1000)})
signature = hmac.new(SECRET.encode(), query.encode(), hashlib.sha256).hexdigest()
url = f"{BASE}/api/v3/account?{query}&signature={signature}"
req = urllib.request.Request(url, headers={"X-MBX-APIKEY": API_KEY})
try:
with urllib.request.urlopen(req, timeout=10) as r:
print(json.loads(r.read())["balances"])
except urllib.error.HTTPError as e:
print(e.code, e.read().decode())
我們用一組格式正確、但並未在幣安登記的假 Key 執行,測試網返回:
401 {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."}
換成你自己在 testnet.binance.vision 生成的 Key,就會打印出測試網帳戶裡的非零餘額。omitZeroBalances=true 是幣安文件裡的可選引數,用來只返回餘額不為零的幣種。注意簽名必須在所有引數(包括 timestamp)都確定之後再計算,signature 自己不參與簽名。
幣安的 Ed25519 簽名有什麼不同?
如果你用的是自行生成的 Ed25519 Key,簽名原文的構造方法完全一樣,只是第三步換成用私鑰簽名,結果做 Base64(再做百分號編碼後放進 signature)。Python 標準庫沒有 Ed25519,需要 pip install cryptography:
import base64
from cryptography.hazmat.primitives.serialization import load_pem_private_key
with open("test-prv-key.pem", "rb") as f:
private_key = load_pem_private_key(f.read(), password=None)
payload = "symbol=BTCUSDT&side=SELL&type=LIMIT&timeInForce=GTC&quantity=1&price=0.2×tamp=1668481559918&recvWindow=5000"
signature = base64.b64encode(private_key.sign(payload.encode("ascii"))).decode()
print(signature)
幣安文件沒有公開 Ed25519 示例私鑰,所以我們用 OpenSSL 本地生成了一對測試金鑰:上面的 Python 程式碼、openssl pkeyutl 和 CCXT 三者算出的簽名完全一致,並且用公鑰驗籤通過。Ed25519 簽名是確定性的,同一私鑰、同一原文每次結果都相同。
歐易 API 簽名怎麼算?
歐易文件的規則是:
- 拼接簽名原文:
timestamp + method + requestPath + body。 - 用 Secret Key 對原文做 HMAC-SHA256。
- 把結果的原始位元組做 Base64 編碼,放進請求頭
OK-ACCESS-SIGN。
四個部分各有講究:
| 部分 | 要求 | 示例 |
|---|---|---|
| timestamp | ISO 8601 格式的 UTC 時間,精確到毫秒,必須和請求頭 OK-ACCESS-TIMESTAMP 相同 |
2020-12-08T09:08:57.715Z |
| method | 大寫 | GET、POST |
| requestPath | 介面路徑;GET 請求的查詢引數也算在裡面 | /api/v5/account/balance?ccy=BTC |
| body | 請求體 JSON 原文;GET 請求沒有請求體,用空字串 | {"instId":"BTC-USDT","lever":"5","mgnMode":"isolated"} |
歐易文件給出了示例 SecretKey 22582BD0CFF14C41EDBF1AB98506286D、示例時間戳和示例請求,但沒有公佈對應的簽名結果。所以我們用文件的示例值計算,再用 OpenSSL 和 CCXT 兩種獨立實現交叉驗證:
import hmac
import hashlib
import base64
import json
# 歐易官方文件中的示例 SecretKey 與時間戳(僅用於演示)
secret = "22582BD0CFF14C41EDBF1AB98506286D"
timestamp = "2020-12-08T09:08:57.715Z"
def okx_sign(timestamp, method, request_path, body, secret):
prehash = timestamp + method.upper() + request_path + body
digest = hmac.new(secret.encode(), prehash.encode(), hashlib.sha256).digest()
return prehash, base64.b64encode(digest).decode()
# 例 1:GET 請求,查詢引數屬於 requestPath,body 為空字串
prehash, sign = okx_sign(timestamp, "GET", "/api/v5/account/balance?ccy=BTC", "", secret)
print("prehash:", prehash)
print("sign :", sign)
# 例 2:POST 請求,body 是傳送出去的 JSON 原文
body = json.dumps({"instId": "BTC-USDT", "lever": "5", "mgnMode": "isolated"}, separators=(",", ":"))
prehash, sign = okx_sign(timestamp, "POST", "/api/v5/account/set-leverage", body, secret)
print("prehash:", prehash)
print("sign :", sign)
執行輸出:
prehash: 2020-12-08T09:08:57.715ZGET/api/v5/account/balance?ccy=BTC
sign : HiZhvSfMtWJA3uUIVXV3a/bSXNPCWvYFXoGCVS8V4zY=
prehash: 2020-12-08T09:08:57.715ZPOST/api/v5/account/set-leverage{"instId":"BTC-USDT","lever":"5","mgnMode":"isolated"}
sign : eCnnCgWLjlQ9XnpUkrcny3qNq3WW/81KNrDr/XR6Xv8=
OpenSSL 交叉驗證,兩條結果與 Python 完全相同:
printf '%s' '2020-12-08T09:08:57.715ZGET/api/v5/account/balance?ccy=BTC' \
| openssl dgst -sha256 -hmac 22582BD0CFF14C41EDBF1AB98506286D -binary | base64
# HiZhvSfMtWJA3uUIVXV3a/bSXNPCWvYFXoGCVS8V4zY=
我們還把 CCXT 的時鐘固定在同一時刻,讓它生成同樣兩個請求,它給出的 OK-ACCESS-SIGN 也分別是 HiZhvSfM…4zY= 和 eCnnCgWL…6Xv8=。三種實現結果一致,說明這套拼接方式就是歐易實際使用的規則。
實際請求時,時間戳要用當前 UTC 時間生成:
from datetime import datetime, timezone
ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
print(ts) # 例如 2026-10-22T09:30:00.123Z
歐易 WebSocket 登入的簽名有什麼不同?
歐易私有 WebSocket 頻道(訂單、帳戶推送)在登入時也要簽名,演算法相同,但有兩處差別:時間戳是 Unix 秒級數字而不是 ISO 時間,method 固定為 GET,requestPath 固定為 /users/self/verify。用文件的示例 SecretKey 和示例時間戳 1704876947 計算:
import base64
import hashlib
import hmac
secret = "22582BD0CFF14C41EDBF1AB98506286D" # 歐易文件示例 SecretKey
timestamp = "1704876947" # 歐易文件示例:Unix 秒級時間戳
prehash = timestamp + "GET" + "/users/self/verify"
sign = base64.b64encode(hmac.new(secret.encode(), prehash.encode(), hashlib.sha256).digest()).decode()
print(prehash)
print(sign)
執行輸出(與 OpenSSL 計算結果一致):
1704876947GET/users/self/verify
5/36BgGV6m/6pmdc20zdqk0mzF5ZalmzzPD2fo3wavU=
登入請求同樣在時間戳 30 秒後過期。REST 和 WebSocket 的時間戳格式不同,是新手寫歐易程式時很容易混淆的地方。
歐易時間戳出錯會怎樣?
歐易文件寫明,時間戳與伺服器時間相差超過 30 秒會返回 50102,最常見的原因是用了本地時區時間而不是 UTC。模擬盤請求的簽名方法完全相同,只是額外加上請求頭 x-simulated-trading: 1,這個請求頭不參與簽名。
幣安和歐易的簽名方式有什麼區別?
| 對比項 | 幣安(HMAC Key) | 歐易 |
|---|---|---|
| 簽名原文 | 查詢字串 + 請求體 | timestamp + method + requestPath + body |
| 時間戳格式 | 毫秒數字,如 1499827319559 | ISO 8601 UTC,如 2020-12-08T09:08:57.715Z |
| 時間容差 | recvWindow,預設 5 秒,最大 60 秒 | 與伺服器相差不超過 30 秒 |
| 輸出編碼 | 十六進位制(不區分大小寫) | Base64 |
| 簽名放在哪 | signature 引數 |
OK-ACCESS-SIGN 請求頭 |
| 其他認證資訊 | X-MBX-APIKEY 請求頭 |
OK-ACCESS-KEY、OK-ACCESS-PASSPHRASE、OK-ACCESS-TIMESTAMP |
| 其他 Key 類型 | 還支援 Ed25519、RSA | 僅 HMAC |
簽名總是報錯,最常見的原因有哪些?
幾乎所有簽名錯誤都可以歸結為一句話:簽名用的字串和實際發出去的字串不一致。逐項排查:
- JSON 多了空格。Python 的
json.dumps預設在冒號和逗號後加空格。我們用同一組資料實測,預設寫法得到的簽名是/XctMG4g…ofI8=,緊湊寫法是eCnnCgWL…6Xv8=,完全不同。最穩妥的做法是先把 body 序列化成字串,簽名和傳送都用這同一個字串。 - 引數順序或格式變了。簽名後又讓 HTTP 庫重新編碼引數,順序可能改變;
0.1被寫成0.10,或空格一處編碼成+、另一處編碼成%20,簽名都會失敗。 - 簽名之後又改了引數。比如先算好籤名,再往請求裡追加一個引數,或者 HTTP 庫自動補了預設引數,簽名就失效了。所有引數都要在簽名之前確定。
- 時間戳不一致。簽名用了一個時間戳,請求頭或引數裡又重新取了一次當前時間。
- 歐易 GET 引數沒算進 requestPath,或請求方法寫成了小寫
get。 - Secret 帶了空白字元。從網頁複製時多了空格或換行,讀取環境變數時可以先
.strip()。 - 用錯了環境的 Key。幣安測試網、歐易模擬盤的 Key 和實盤互不通用,這類錯誤通常報 Key 無效而不是簽名錯誤。
常見問題
幣安 API 簽名為什麼報 -1022?
-1022 表示簽名無效。最常見的原因是簽名用的查詢字串和實際發出去的不一致,比如引數順序變了、數字格式變了、非 ASCII 字元沒有先做百分號編碼,或者 Secret Key 複製時多了空格和換行。
歐易簽名裡的 requestPath 要不要帶查詢引數?
要帶。歐易文件寫明 GET 請求的引數算作 requestPath 的一部分,例如 /api/v5/account/balance?ccy=BTC 要整體參與簽名;POST 請求的引數放在 body 裡,requestPath 只寫路徑。
幣安 recvWindow 設多大合適?
幣安文件建議使用 5000 毫秒或更小的值,不傳時預設就是 5000,最大不能超過 60000。recvWindow 越大,被截獲的請求可被重放的時間視窗越長,網路穩定時不建議調大。
HMAC 簽名和 Ed25519 簽名有什麼區別?
HMAC 是對稱演算法,你和交易所持有同一個 Secret;Ed25519 是非對稱演算法,你用私鑰簽名,交易所只儲存公鑰驗證。幣安兩種都支援,並在文件裡推薦 Ed25519;歐易 API 使用 HMAC-SHA256 簽名。
可以用線上 HMAC 工具算簽名嗎?
用官方文件的示例 Key 練習可以,真實的 Secret Key 絕對不要貼上到任何網頁工具裡。線上工具能看到你輸入的全部內容,等於把 Secret 交給了陌生人,請只在自己電腦上用程式碼計算。
準備好出發了嗎?
參考來源
- Binance Spot API 文件:SIGNED Endpoint Examples / Timing Security
- Binance Spot API 文件:API Key Types
- OKX API v5 文件:REST Authentication / Signature
- Python 官方文件:hmac 模組
以上資訊核實於 2026-10-22,交易所規則可能調整,請以官方最新說明為準。
本文僅供學習參考,不構成投資建議。交易所的費率、活動與功能可能隨時調整,請以官方頁面為準。文中部分連結為推廣連結,詳見推廣披露。