HMAC 认证
本页详细介绍用于认证 Lampay API 请求的完整 HMAC-SHA256 签名计算过程。每个发往 /api/** 端点的请求都必须使用此流程签名。
签名计算
签名计算分三步:对请求体进行哈希、构建规范消息字符串、计算 HMAC。
第 1 步:计算请求体哈希
计算原始请求体的 SHA-256 哈希,并编码为小写十六进制字符串。
bodyHash = lowercase_hex(SHA-256(requestBody))
对于有请求体的请求(POST、PUT、PATCH),对实际发送的 JSON 字符串进行哈希:
echo -n '{"sourceWalletId":"w_123","amount":"100.00"}' | openssl dgst -sha256 -hex
# 输出:a1fce4363854ff888cff4b8e7875d600c...
对于没有请求体的请求(GET、DELETE),使用空字符串的 SHA-256 哈希:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
请求体哈希是基于 HTTP 请求体中发送的精确字节序列计算的。如果您的 JSON 序列化器产生 {"a": 1}(冒号后有空格),但您对 {"a":1}(无空格)进行哈希,签名将不匹配。始终对与传给 HTTP 客户端的相同字符串进行哈希。
第 2 步:构建规范消息
用换行符(\n)连接五个组件:
canonicalMessage = method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash
| 组件 | 说明 | 示例 |
|---|---|---|
method | HTTP 方法,大写 | POST |
path | 仅请求路径 -- 不含协议、主机名或查询字符串 | /api/v1/transfers |
timestamp | Unix epoch 秒数(UTC),与 X-Timestamp 头的值相同 | 1709337600 |
nonce | 唯一标识符,与 X-Nonce 头的值相同(推荐 UUID v4) | 550e8400-e29b-41d4-a716-446655440000 |
bodyHash | 第 1 步中的 SHA-256 十六进制摘要 | a1fce436... |
对于 POST /api/v1/transfers 请求,规范消息如下:
POST
/api/v1/transfers
1709337600
550e8400-e29b-41d4-a716-446655440000
a1fce4363854ff888cff4b8e7875d600c...
对于带查询参数的 GET 请求(例如 /api/v1/wallets?page=0&size=20),规范消息中只包含路径部分:/api/v1/wallets。查询参数不参与签名计算。
第 3 步:计算 HMAC 签名
使用 API Secret 对规范消息进行 HMAC-SHA256 签名,然后对结果进行 Base64 编码:
signature = Base64(HMAC-SHA256(canonicalMessage, apiSecret))
生成的 Base64 字符串放入 X-Signature 头。
完整示例
Bash + curl
以下脚本演示了一个完整的签名 API 请求:
#!/bin/bash
# Lampay API 对接 -- HMAC-SHA256 认证示例
# ─── 配置 ────────────────────────────────────────────────────
API_KEY="sk_live_abc123def456"
API_SECRET="your-api-secret-here"
BASE_URL="https://api.lampay.app"
# ─── 请求参数 ────────────────────────────────────────────────
METHOD="POST"
PATH_URI="/api/v1/transfer/command/create"
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
# 请求体(必须与实际发送的字符串完全一致)
BODY='{"sourceWalletId":"w_123","targetWalletId":"w_456","amount":"100.00","currency":"USD"}'
# ─── 第 1 步:请求体哈希 ─────────────────────────────────────
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
# ─── 第 2 步:规范消息 ──────────────────────────────────────
CANONICAL="${METHOD}\n${PATH_URI}\n${TIMESTAMP}\n${NONCE}\n${BODY_HASH}"
# ─── 第 3 步:HMAC-SHA256 签名 ──────────────────────────────
SIGNATURE=$(echo -ne "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | openssl base64 -A)
# ─── 发送请求 ────────────────────────────────────────────────
curl -X "$METHOD" "${BASE_URL}${PATH_URI}" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $API_KEY" \
-H "X-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-d "$BODY"
对于 GET 请求,省略 -d 参数并使用空字符串的请求体哈希:
METHOD="GET"
PATH_URI="/api/v1/wallets"
BODY=""
BODY_HASH="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
CANONICAL="${METHOD}\n${PATH_URI}\n${TIMESTAMP}\n${NONCE}\n${BODY_HASH}"
SIGNATURE=$(echo -ne "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | openssl base64 -A)
curl -X "$METHOD" "${BASE_URL}${PATH_URI}" \
-H "X-Api-Key: $API_KEY" \
-H "X-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE"
Node.js
一个可复用的签名和发送 API 请求函数:
import crypto from 'crypto';
/**
* 发送经过认证的请求到 Lampay API。
*
* @param {string} method - HTTP 方法(GET、POST、PUT、DELETE)
* @param {string} path - 请求路径(例如 /api/v1/wallets)
* @param {object|null} body - 请求体(GET/DELETE 为 null)
* @param {string} apiKey - 您的 API 密钥(sk_live_xxx)
* @param {string} apiSecret - 您的 API Secret
* @returns {Promise<object>} 解析后的 JSON 响应
*/
async function apiRequest(method, path, body, apiKey, apiSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
// 第 1 步:请求体哈希
const bodyStr = body ? JSON.stringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(bodyStr).digest('hex');
// 第 2 步:规范消息
const canonical = [method, path, timestamp, nonce, bodyHash].join('\n');
// 第 3 步:HMAC-SHA256 签名
const signature = crypto
.createHmac('sha256', apiSecret)
.update(canonical)
.digest('base64');
// 发送请求
const res = await fetch(`https://api.lampay.app${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': apiKey,
'X-Signature': signature,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
},
body: bodyStr || undefined,
});
return res.json();
}
// ─── 使用示例 ──────────────────────────────────────────────
// POST:创建转账
const transfer = await apiRequest(
'POST',
'/api/v1/transfer/command/create',
{
sourceWalletId: 'w_123',
targetWalletId: 'w_456',
amount: '100.00',
currency: 'USD',
},
'sk_live_abc123def456',
'your-api-secret-here'
);
// GET:查询钱包列表(无请求体)
const wallets = await apiRequest(
'GET',
'/api/v1/wallets',
null,
'sk_live_abc123def456',
'your-api-secret-here'
);
服务器验证流水线
服务器收到 API 请求时,通过多步流水线进行验证。任一步骤失败将短路处理并返回相应的错误码。
┌─────────────────────────────────────────────────────────────────┐
│ 验证流水线 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. API 密钥查找 │
│ ├── 缺少 X-Api-Key 头 → GA2001 (401) │
│ └── 密钥未找到 → GA2011 (401) │
│ │
│ 2. 密钥状态检查 │
│ └── 管理员已禁用密钥 → GA2021 (403) │
│ │
│ 3. 工作空间解析 │
│ ├── 工作空间未找到 → GA2032 (404) │
│ └── 非工作空间成员 → GA2034 (403) │
│ │
│ 4. 时间戳验证 │
│ └── |服务器时间 - 时间戳| > 60秒 → GA2013 (401) │
│ │
│ 5. Nonce 唯一性检查 │
│ └── Nonce 已被使用 → GA2013 (401) │
│ │
│ 6. HMAC 签名验证 │
│ └── 计算的 HMAC ≠ X-Signature → GA2012 (401) │
│ │
│ 7. IP 白名单(可选) │
│ └── 客户端 IP 不在白名单中 → GA2022 (403) │
│ │
│ 8. 权限范围检查 │
│ └── 缺少所需权限范围 → GA2024 (403) │
│ │
│ ✓ 所有检查通过 → 处理请求 │
└─────────────────────────────────────────────────────────────────┘
流水线按固定顺序验证。例如,如果您的 API 密钥有效但时间戳过期,您将收到 GA2013(时间戳过期)而非签名错误。使用错误码精确诊断哪一步失败。
响应示例
成功的 API 响应:
认证失败:
API 权限范围
API 密钥是工作空间级别的 — 每个密钥属于一个工作空间,只能访问该工作空间内的资源。无法通过 API 密钥进行跨工作空间访问。
每个 API 密钥分配一个或多个权限范围(scope),控制其可以访问的端点:
| 权限范围 | 说明 |
|---|---|
wallet:read | 查询工作空间内的钱包 |
transfer:read | 查询工作空间内的转账记录 |
transfer:create | 在工作空间内的钱包之间创建转账订单 |
可访问端点
| 权限范围 | 方法 | 端点 | 说明 |
|---|---|---|---|
wallet:read | GET | /api/v1/wallets | 钱包列表 |
wallet:read | GET | /api/v1/wallets/{id} | 钱包详情 |
wallet:read | GET | /api/v1/wallets/{id}/balance | 钱包余额 |
wallet:read | GET | /api/v1/wallets/{id}/flows | 钱包流水 |
transfer:read | GET | /api/v1/transfer/query/orders | 转账订单列表 |
transfer:read | GET | /api/v1/transfer/query/orders/wallet/{walletId} | 按钱包查询转账 |
transfer:read | GET | /api/v1/transfer/query/orders/{bizId} | 转账订单详情 |
transfer:read | GET | /api/v1/transfer/query/orders/{bizId}/completed | 检查转账完成状态 |
transfer:create | POST | /api/v1/transfer/command/create | 创建转账订单 |
未声明 API 权限范围的端点无法通过 API 链访问(默认拒绝)。对此类端点的请求,或缺少所需权限范围的请求,将收到 403 Forbidden 响应,错误码为 50090201 (API_ACCESS_DENIED)。
工作空间隔离
所有 API 响应自动限定在 API 密钥绑定的工作空间范围内。转账查询端点强制执行工作空间归属验证 — 如果转账订单属于其他工作空间,请求将返回错误。即使使用有效的订单 ID,也能防止跨工作空间的数据泄露。
常见陷阱
服务器拒绝时间戳与服务器时间相差超过 60 秒的请求。如果遇到 GA2013 错误,请检查您的服务器时钟是否通过 NTP 同步。本地调试时,将您机器上的 date +%s 与 NTP 服务器进行比较。
GET 和 DELETE 请求没有请求体。您仍然必须在规范消息中包含请求体哈希 -- 使用空字符串的 SHA-256 值:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。省略或使用其他值将导致 GA2012。
请求体哈希覆盖请求体的精确字节。如果您对一个 JSON 字符串进行哈希但 HTTP 库重新序列化了请求体(改变键顺序、空格或编码),签名将不匹配。最佳实践:序列化一次,对该字符串进行哈希,并发送同一字符串作为请求体。
每个 nonce 在 60 秒时间戳窗口内必须全局唯一。重复使用 nonce(即使请求体不同)将导致 GA2013。使用 UUID v4 以保证唯一性。
后续步骤
- API 参考与错误码 -- 完整的错误码表、API 密钥管理和技术规范。
- 错误处理 -- 响应信封格式和跨所有域的完整错误码目录。