认证
访问 Fystack API 的任何 endpoint 都需要对请求进行认证。本指南将介绍认证的工作原理。Fystack 使用基于 HMAC 的认证来安全地验证 API 请求的真实性。
HMAC 认证
Fystack API 使用安全的、基于 HMAC 的认证体系,要求你使用 API secret 对请求进行签名。每个请求都需要三个认证请求头:
Required authentication headers
ACCESS-API-KEY: your_api_key
ACCESS-TIMESTAMP: current_unix_timestamp
ACCESS-SIGN: base64_encoded_hmac_signature
HMAC 签名的生成方式是:将 HTTP 方法、请求路径、时间戳以及请求体(如有)组合起来,然后使用你的 API secret 进行签名。
带认证请求头的请求示例
以下示例展示了如何向 Fystack API 发送经过认证的请求:
Example authenticated request
curl https://api.fystack.io/api/v1/wallets \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature=="
计算 ACCESS-SIGN
计算 HMAC 签名需要以下步骤:
- 按固定顺序使用请求参数构建规范字符串:
method、path、timestamp、body - 使用你的 API secret 以 HMAC-SHA256 对该字符串签名
- 将 HMAC 摘要进行十六进制编码,再对该十六进制字符串进行 base64 编码
请始终妥善保管你的 API secret,如怀疑已泄露请立即重置。以下是各语言计算签名的方式:
import CryptoJS from 'crypto-js'
function computeSignature(
apiSecret: string,
method: string,
path: string,
timestamp: string,
body: string = ''
): string {
// Build canonical string in exact order
const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`
// HMAC-SHA256 -> hex -> base64
const hexDigest = CryptoJS.HmacSHA256(canonical, apiSecret)
.toString(CryptoJS.enc.Hex)
return btoa(hexDigest)
}
// Example usage
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = computeSignature(
'your_api_secret', 'GET',
'/api/v1/workspaces/your_workspace_id/wallets',
timestamp
)
const headers = {
'ACCESS-API-KEY': 'your_api_key',
'ACCESS-TIMESTAMP': timestamp,
'ACCESS-SIGN': signature
}
请求签名流程
-
按固定顺序构建规范字符串:
method={METHOD}&path={PATH}×tamp={TIMESTAMP}&body={BODY}method:大写的 HTTP 方法(GET、POST 等)path:请求路径(例如/api/v1/workspaces/{id}/wallets)timestamp:以秒为单位的 UNIX 时间戳body:JSON 序列化后的请求体,GET 请求则为空字符串
-
生成 HMAC-SHA256 签名:
- 使用你的 API secret 对规范字符串签名
- 对摘要进行十六进制编码
- 对十六进制字符串进行 base64 编码
-
添加必需的请求头:
ACCESS-API-KEY:你的 API keyACCESS-TIMESTAMP:签名中使用的时间戳ACCESS-SIGN:base64 编码后的签名
请妥善保管你的 API secret,切勿与他人共享。API key 可以共享,但 secret 必须保密。
构建 Body 字符串
无论是 HMAC 还是 Ed25519,规范字符串中的 body 都必须与你实际发送的字节完全一致。对于 GET 请求或任何没有请求体的请求,它应为空字符串,绝不能是 "{}" 或 null。
需要避免的错误:对 payload 序列化两次,一次用于签名,一次在 HTTP 客户端发送时。如果你签名的内容与服务器收到的内容在字节上不完全一致,即使 payload 在逻辑上是相同的 JSON,ValidateAccessSign 也会失败。请只序列化一次,然后将这个完全相同的字符串既用于构建规范字符串,也作为实际的请求体;不要把原始的 object/dict/map 交给 HTTP 客户端,并指望第二次独立的序列化调用能精确复现它。根据语言的不同,这要么会明显报错(某些 HTTP 客户端会直接拒绝或错误处理非字符串的请求体),要么会悄无声息地表现为难以排查的签名不匹配(当两次序列化调用因库升级、默认选项不同或 NaN 等特殊值而产生差异时),因此不要依赖任何一种结果,只需避免重复序列化。
const payload = { name: 'my-integration' }
const body = JSON.stringify(payload) // serialize once
// sign() uses `body` in the canonical string, exactly as below
const signature = computeSignature(apiSecret, 'POST', path, timestamp, body)
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', /* ...auth headers */ },
// `fetch` requires body to already be a string (or Blob/ArrayBuffer/etc),
// it will not JSON-serialize `payload` for you, pass `body`, not `payload`
body
})
Ed25519 签名
如果你的 API key 使用的是客户端密钥对而不是 HMAC secret,请使用你的 Ed25519 私钥对同一个规范字符串签名,而不是计算 HMAC。与 HMAC 流程相比有两点不同:
- 直接对规范字符串的字节签名,没有单独的哈希步骤,Ed25519 会在内部完成哈希。
- 直接对原始的 64 字节签名进行 base64 编码,没有十六进制编码步骤。
ACCESS-SIGN = base64( Ed25519_Sign(privateKey, canonical_string) )
如果你使用的是 @fystack/sdk,只需在 credentials 中传入一个 signer(LocalPrivateKeySigner 或 AwsKmsSigner),SDK 就会为你完成签名,详见 API Keys。以下示例适用于不使用 SDK、以其他语言进行签名的场景。
const crypto = require('crypto');
function computeSignature(privateKeyPem, method, path, timestamp, body = '') {
const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`;
const privateKey = crypto.createPrivateKey(privateKeyPem);
// No digest algorithm, Ed25519 signs the message directly
const signature = crypto.sign(null, Buffer.from(canonical), privateKey);
return signature.toString('base64');
}
// Usage
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = computeSignature(
process.env.PRIVATE_KEY_PEM, 'GET',
'/api/v1/workspaces/your_workspace_id/wallets',
timestamp
);
切勿将你的 Ed25519 私钥发送给 Fystack,也不要将其嵌入客户端或浏览器代码中,注册时只需提供公钥。生产环境中,请将密钥保存在 AWS KMS 等硬件级存储中,而不是 PEM 文件中,详见客户端密钥对。
通过 AWS KMS 签名(不使用 SDK)
如果你在 Node.js 上使用 @fystack/sdk,AwsKmsSigner 已经为你完成了这一步,详见客户端密钥对。对于其他所有后端,或者你只是想了解其机制,同样的方法适用于任何语言的 AWS SDK:使用 MessageType: RAW 和 SigningAlgorithm: ED25519_SHA_512 对原始规范字符串调用 KMS Sign,然后对返回的签名字节进行 base64 编码。这正是 AwsKmsSigner 的内部实现,下面的 Node.js 示例就是直接写出的相同逻辑。
MessageType 必须为 RAW,而不是 DIGEST。KMS 的 ED25519_SHA_512 算法要求传入未经哈希的消息并在内部进行哈希,这与本页其他部分中 Ed25519 的工作方式一致。传入 DIGEST 会对一个不同的、无法互通的值进行签名。
import { KMSClient, SignCommand } from '@aws-sdk/client-kms'
async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') {
const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`
const response = await kmsClient.send(new SignCommand({
KeyId: keyId,
Message: Buffer.from(canonical, 'utf8'),
MessageType: 'RAW',
SigningAlgorithm: 'ED25519_SHA_512'
}))
return Buffer.from(response.Signature).toString('base64')
}
上面的 kmsClient/client/$kms 就是你所用语言的 AWS SDK 提供的 KMS 客户端,按你平时的方式配置即可。本地与生产环境的通用配置模式请参阅客户端密钥对(本地将其指向 LocalStack/ministack 并使用静态测试凭证,生产环境去掉这两项,由 IAM 角色提供凭证)。无论使用哪种语言,思路都相同,只是通过各 SDK 自身的配置选项来实现。