HMAC 签名

每个 API 请求都通过 HMAC-SHA256 请求签名进行认证。没有长期有效的 bearer token;每个请求都携带全新的签名,因此无法被重放。

签名头

头说明
x-api-key你的 access key id(公开)。
x-timestamp请求创建时的 Unix 时间(秒)。
x-nonce每个请求唯一的随机字符串(例如 16 随机字节的 hex)。
x-body-hashhex(sha256(rawBody)) —— 请求体原始字节的哈希。
x-signaturehex(HMAC-SHA256(secret, canonicalString))。

规范字符串(canonical string)

签名基于以下字符串计算,各段用 \n 连接:

TEA1-HMAC-SHA256
<METHOD>
<path>
<canonicalQuery>
<timestamp>
<nonce>
<bodyHash>
  • <METHOD> 大写(GET / POST)。
  • <path> 是包含 /api/v1 前缀的完整路径——例如 /api/v1/energy/quotes。
  • <canonicalQuery> 是排序并重新编码后的查询字符串;无查询串时为空。
  • <timestamp>、<nonce>、<bodyHash> 与上面请求头中的一致。

两个容易踩的坑

  1. 路径包含 /api/v1。 签名的是 /api/v1/energy/quotes,而不是 /energy/quotes。
  2. x-body-hash 永远不能省略。 对 GET 请求(以及任何空 body)而言,它是 sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。

TypeScript 示例

import { createHash, createHmac, randomBytes } from 'node:crypto';

const ALGORITHM = 'TEA1-HMAC-SHA256';

function sha256Hex(data: string | Buffer): string {
  return createHash('sha256').update(data).digest('hex');
}

function sign(secret: string, method: string, path: string, body = ''): Record<string, string> {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = randomBytes(16).toString('hex');
  const bodyHash = sha256Hex(body);
  const canonical = [ALGORITHM, method.toUpperCase(), path, '', timestamp, nonce, bodyHash].join('\n');
  const signature = createHmac('sha256', secret).update(canonical).digest('hex');
  return { timestamp, nonce, bodyHash, signature };
}

const body = JSON.stringify({ amount: 64400, duration_minutes: 60 });
const signed = sign(process.env.API_SECRET!, 'POST', '/api/v1/energy/quotes', body);

const res = await fetch('https://api.hexvetron.com/api/v1/energy/quotes', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-api-key': process.env.API_KEY_ID!,
    'x-timestamp': signed.timestamp,
    'x-nonce': signed.nonce,
    'x-body-hash': signed.bodyHash,
    'x-signature': signed.signature,
  },
  body,
});

防重放

  • x-timestamp 必须落在服务器时钟的防重放窗口内(几分钟)。
  • x-nonce 在服务端一次性消费;同一 nonce 在窗口内不能重复使用。
  • body 哈希会与收到的原始字节再次比对,因此被篡改的 body 会校验失败。

请求 id

可选地发送 x-request-id 头,用于把响应关联到你的日志。它会在信封的 request_id 中原样返回。