本文档说明 Legend 如何对发往交易所的 API 请求进行签名,以及交易所应如何验证签名以确认请求来源的真实性。
概述
Legend 向交易所发送的每个 HTTP 请求都包含以下认证请求头:
| 请求头 | 说明 |
|---|---|
api-key | Legend 颁发的静态 API Key,用于标识应用身份 |
appId | Legend 内部应用标识符 |
Invoke-Time | 请求时间戳,单位为毫秒 |
Invoke-Signature | 请求签名,HMAC-SHA256 算法,Base64 编码 |
交易所在处理每个请求前,必须先验证 Invoke-Signature。
签名算法
message = url_query_string(按 key 字母升序排列的参数) + Invoke-Time
signature = Base64( HMAC-SHA256-binary(message, app_secret) )
逐步说明:
- 收集所有请求参数(即 Legend 发送的 POST Body 或 Query String 字段)。
- 按 key 字母升序排列参数(区分大小写)。
- 拼接 URL 编码的查询字符串,采用标准
application/x-www-form-urlencoded格式,例如amount=100&pair=BTC-USD&uid=u123。 - 直接追加
Invoke-Time(取自请求头的值),不加任何分隔符。- 示例 message:
amount=100&pair=BTC-USD&uid=u1231714000000000
- 示例 message:
- 计算 HMAC-SHA256,以
app_secret为密钥对 message 进行签名,使用原始二进制输出(非十六进制字符串)。 - 对二进制摘要进行 Base64 编码,结果即为期望的
Invoke-Signature值。 - 对比签名,若与请求头中的
Invoke-Signature不一致,则拒绝该请求。
防重放攻击
为防止重放攻击,若 Invoke-Time 与服务器当前时间偏差超过 5 分钟(300,000 毫秒),应拒绝该请求。
abs(当前时间戳_ms - Invoke-Time) > 300_000 → 拒绝请求
代码示例
PHP
function verifySignature(array $params, string $invokeTime, string $invokeSignature, string $appSecret): bool
{
ksort($params);
$message = http_build_query($params) . $invokeTime;
$expected = base64_encode(hash_hmac('sha256', $message, $appSecret, true));
return hash_equals($expected, $invokeSignature);
}Python
import hmac, hashlib, base64
from urllib.parse import urlencode
def verify_signature(params: dict, invoke_time: str, invoke_signature: str, app_secret: str) -> bool:
sorted_params = dict(sorted(params.items()))
message = urlencode(sorted_params) + invoke_time
expected = base64.b64encode(
hmac.new(app_secret.encode(), message.encode(), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, invoke_signature)Node.js
const crypto = require('crypto');
const qs = require('querystring');
function verifySignature(params, invokeTime, invokeSignature, appSecret) {
const sorted = Object.fromEntries(Object.entries(params).sort());
const message = qs.stringify(sorted) + invokeTime;
const expected = crypto
.createHmac('sha256', appSecret)
.update(message)
.digest('base64');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(invokeSignature));
}Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
import java.net.URLEncoder;
import java.util.Base64;
public boolean verifySignature(Map<String, String> params, String invokeTime,
String invokeSignature, String appSecret) throws Exception {
TreeMap<String, String> sorted = new TreeMap<>(params); // TreeMap 自动按 key 排序
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sorted.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
sb.append(invokeTime);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256"));
String expected = Base64.getEncoder().encodeToString(mac.doFinal(sb.toString().getBytes("UTF-8")));
return MessageDigest.isEqual(expected.getBytes(), invokeSignature.getBytes());
}请求示例
Legend 向 https://your-exchange.com/trade-notify 发送如下 POST 请求:
请求头:
api-key: your_api_key
appId: your_app_id
Invoke-Time: 1714000000000
Invoke-Signature: X5v2kL+...base64string...==
Content-Type: application/x-www-form-urlencoded
请求参数:
uid=u123
trade_id=TRD-001
pair=BTC-USD
side=buy
price=65000.00
quantity=0.01
size=650.00
trade_status=started
签名构造过程:
# 1. 按 key 字母升序排列参数:
pair=BTC-USD&price=65000.00&quantity=0.01&side=buy&size=650.00&trade_id=TRD-001&trade_status=started&uid=u123
# 2. 追加 Invoke-Time:
pair=BTC-USD&price=65000.00&quantity=0.01&side=buy&size=650.00&trade_id=TRD-001&trade_status=started&uid=u1231714000000000
# 3. HMAC-SHA256(二进制)→ Base64:
Invoke-Signature: X5v2kL+...==
密钥凭证
Legend 将通过安全渠道向交易所提供以下凭证:
| 凭证 | 说明 |
|---|---|
app_secret | 共享密钥,用于签名和验签 |
api_key | 每个请求头中携带的 API Key |
app_id | 每个请求头中携带的应用标识符 |
请务必妥善保管 app_secret,不得记录到日志或暴露在任何客户端代码中。
