Webhook请求签名验证

本文档说明 Legend 如何对发往交易所的 API 请求进行签名,以及交易所应如何验证签名以确认请求来源的真实性。


概述

Legend 向交易所发送的每个 HTTP 请求都包含以下认证请求头:

请求头说明
api-keyLegend 颁发的静态 API Key,用于标识应用身份
appIdLegend 内部应用标识符
Invoke-Time请求时间戳,单位为毫秒
Invoke-Signature请求签名,HMAC-SHA256 算法,Base64 编码

交易所在处理每个请求前,必须先验证 Invoke-Signature


签名算法

message   = url_query_string(按 key 字母升序排列的参数) + Invoke-Time
signature = Base64( HMAC-SHA256-binary(message, app_secret) )

逐步说明:

  1. 收集所有请求参数(即 Legend 发送的 POST Body 或 Query String 字段)。
  2. 按 key 字母升序排列参数(区分大小写)。
  3. 拼接 URL 编码的查询字符串,采用标准 application/x-www-form-urlencoded 格式,例如 amount=100&pair=BTC-USD&uid=u123
  4. 直接追加 Invoke-Time(取自请求头的值),不加任何分隔符。
    • 示例 message:amount=100&pair=BTC-USD&uid=u1231714000000000
  5. 计算 HMAC-SHA256,以 app_secret 为密钥对 message 进行签名,使用原始二进制输出(非十六进制字符串)。
  6. 对二进制摘要进行 Base64 编码,结果即为期望的 Invoke-Signature 值。
  7. 对比签名,若与请求头中的 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不得记录到日志或暴露在任何客户端代码中