HMAC 签名
每个 API 请求都通过 HMAC-SHA256 请求签名进行认证。没有长期有效的 bearer token;每个请求都携带全新的签名,因此无法被重放。
签名头
| 头 | 说明 |
|---|---|
x-api-key | 你的 access key id(公开)。 |
x-timestamp | 请求创建时的 Unix 时间(秒)。 |
x-nonce | 每个请求唯一的随机字符串(例如 16 随机字节的 hex)。 |
x-body-hash | hex(sha256(rawBody)) —— 请求体原始字节的哈希。 |
x-signature | hex(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>与上面请求头中的一致。
两个容易踩的坑
- 路径包含
/api/v1。 签名的是/api/v1/energy/quotes,而不是/energy/quotes。 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 中原样返回。
