---
title: "认证"
description: "本指南介绍认证的工作原理。Fystack API 使用基于 HMAC 的认证来安全地验证你的 API 请求。"
url: https://docs.fystack.io/zh/authentication
last_updated: 2026-11-13
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# 认证

访问 Fystack API 的任何 endpoint 都需要对请求进行认证。本指南将介绍认证的工作原理。Fystack 使用基于 HMAC 的认证来安全地验证 API 请求的真实性。

## HMAC 认证

Fystack API 使用安全的、基于 HMAC 的认证体系，要求你使用 API secret 对请求进行签名。每个请求都需要三个认证请求头：

```bash
ACCESS-API-KEY: your_api_key
ACCESS-TIMESTAMP: current_unix_timestamp
ACCESS-SIGN: base64_encoded_hmac_signature
```

HMAC 签名的生成方式是：将 HTTP 方法、请求路径、时间戳以及请求体（如有）组合起来，然后使用你的 API secret 进行签名。

## 带认证请求头的请求示例

以下示例展示了如何向 Fystack API 发送经过认证的请求：

```bash
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 签名需要以下步骤：

1. 按固定顺序使用请求参数构建规范字符串：`method`、`path`、`timestamp`、`body`
2. 使用你的 API secret 以 HMAC-SHA256 对该字符串签名
3. 将 HMAC 摘要进行十六进制编码，再对该十六进制字符串进行 base64 编码

请始终妥善保管你的 API secret，如怀疑已泄露请立即重置。以下是各语言计算签名的方式：

```typescript
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}&timestamp=${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
}
```

```go
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/base64"
  "encoding/hex"
  "fmt"
  "strconv"
  "time"
)

func computeSignature(apiSecret string, method, path, timestamp, body string) string {
  // Build canonical string in fixed order
  message := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s",
    method, path, timestamp, body)

  // Create HMAC-SHA256
  h := hmac.New(sha256.New, []byte(apiSecret))
  h.Write([]byte(message))
  hexSig := hex.EncodeToString(h.Sum(nil))

  // Base64-encode the hex string
  return base64.StdEncoding.EncodeToString([]byte(hexSig))
}

// Usage
ts := strconv.FormatInt(time.Now().Unix(), 10)
signature := computeSignature(
  apiSecret, "GET",
  "/api/v1/workspaces/your_workspace_id/wallets",
  ts, "",
)
```

```php
<?php
function computeSignature($apiSecret, $method, $path, $timestamp, $body = '') {
  // Build canonical string in fixed order
  $message = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  // Create HMAC-SHA256 hex digest
  $hexSig = hash_hmac('sha256', $message, $apiSecret);

  // Base64-encode the hex string
  return base64_encode($hexSig);
}

// Usage
$timestamp = time();
$signature = computeSignature(
  $apiSecret, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  $timestamp, ''
);

$headers = [
  'ACCESS-API-KEY: ' . $apiKey,
  'ACCESS-TIMESTAMP: ' . $timestamp,
  'ACCESS-SIGN: ' . $signature
];
```

```python
import hmac
import hashlib
import time
import base64

def compute_signature(api_secret: str, method: str, path: str, timestamp: str, body: str = '') -> str:
  # Build canonical string in fixed order
  message = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  # Create HMAC-SHA256 hex digest
  hex_sig = hmac.new(
    api_secret.encode(),
    message.encode(),
    hashlib.sha256
  ).hexdigest()

  # Base64-encode the hex string
  return base64.b64encode(hex_sig.encode()).decode()

# Usage
timestamp = str(int(time.time()))
signature = compute_signature(
  api_secret, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp, ''
)

headers = {
  'ACCESS-API-KEY': api_key,
  'ACCESS-TIMESTAMP': timestamp,
  'ACCESS-SIGN': signature
}
```

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;

public static String computeSignature(String apiSecret, String method, String path, String timestamp, String body) throws Exception {
  // Build canonical string in fixed order
  String canonical = String.format("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body);

  // Create HMAC-SHA256
  Mac mac = Mac.getInstance("HmacSHA256");
  mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
  byte[] digest = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));

  // Hex-encode the digest
  StringBuilder hexDigest = new StringBuilder();
  for (byte b : digest) {
    hexDigest.append(String.format("%02x", b));
  }

  // Base64-encode the hex string
  return Base64.getEncoder().encodeToString(hexDigest.toString().getBytes(StandardCharsets.UTF_8));
}

// Usage
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String signature = computeSignature(
  apiSecret, "GET",
  "/api/v1/workspaces/your_workspace_id/wallets",
  timestamp, ""
);

Map<String, String> headers = Map.of(
  "ACCESS-API-KEY", apiKey,
  "ACCESS-TIMESTAMP", timestamp,
  "ACCESS-SIGN", signature
);
```

### 请求签名流程

1. 按固定顺序构建规范字符串：`method={METHOD}&path={PATH}&timestamp={TIMESTAMP}&body={BODY}`
   - `method`：大写的 HTTP 方法（GET、POST 等）
   - `path`：请求路径（例如 `/api/v1/workspaces/{id}/wallets`）
   - `timestamp`：以秒为单位的 UNIX 时间戳
   - `body`：JSON 序列化后的请求体，GET 请求则为空字符串

2. 生成 HMAC-SHA256 签名：
   - 使用你的 API secret 对规范字符串签名
   - 对摘要进行十六进制编码
   - 对十六进制字符串进行 base64 编码

3. 添加必需的请求头：
   - `ACCESS-API-KEY`：你的 API key
   - `ACCESS-TIMESTAMP`：签名中使用的时间戳
   - `ACCESS-SIGN`：base64 编码后的签名

> **Warning.** 请妥善保管你的 API secret，切勿与他人共享。API key 可以共享，但 secret 必须保密。

## 构建 Body 字符串

无论是 HMAC 还是 Ed25519，规范字符串中的 `body` 都必须与**你实际发送的字节完全一致**。对于 `GET` 请求或任何没有请求体的请求，它应为空字符串，绝不能是 `"{}"` 或 `null`。

需要避免的错误：对 payload 序列化两次，一次用于签名，一次在 HTTP 客户端发送时。如果你签名的内容与服务器收到的内容在字节上不完全一致，即使 payload 在逻辑上是相同的 JSON，`ValidateAccessSign` 也会失败。请只序列化一次，然后将这个完全相同的字符串既用于构建规范字符串，也作为实际的请求体；不要把原始的 object/dict/map 交给 HTTP 客户端，并指望第二次独立的序列化调用能精确复现它。根据语言的不同，这要么会明显报错（某些 HTTP 客户端会直接拒绝或错误处理非字符串的请求体），要么会悄无声息地表现为难以排查的签名不匹配（当两次序列化调用因库升级、默认选项不同或 `NaN` 等特殊值而产生差异时），因此不要依赖任何一种结果，只需避免重复序列化。

```javascript
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
})
```

```go
payload := map[string]string{"name": "my-integration"}
bodyBytes, _ := json.Marshal(payload) // serialize once
body := string(bodyBytes)

signature := computeSignature(apiSecret, "POST", path, timestamp, body)

req, _ := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) // same bytes
```

```python
import json
import requests

payload = {'name': 'my-integration'}
body = json.dumps(payload)  # serialize once

signature = compute_signature(api_secret, 'POST', path, timestamp, body)

# Pass data=body (the exact string you signed). Passing json=payload
# instead would have requests() serialize the dict itself, a second,
# independent call that isn't guaranteed to match what you signed
requests.post(url, data=body, headers={'Content-Type': 'application/json', **auth_headers})
```

```php
<?php
$payload = ['name' => 'my-integration'];
$body = json_encode($payload); // serialize once

$signature = computeSignature($apiSecret, 'POST', $path, $timestamp, $body);

curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // same string
```

```java
String body = objectMapper.writeValueAsString(payload); // serialize once

String signature = computeSignature(apiSecret, "POST", path, timestamp, body);

HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create(url))
  .POST(HttpRequest.BodyPublishers.ofString(body)) // same string
  .build();
```

## Ed25519 签名

如果你的 API key 使用的是[客户端密钥对](https://docs.fystack.io/api-keys.md#client-key-pair)而不是 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](https://docs.fystack.io/api-keys.md#client-key-pair)。以下示例适用于不使用 SDK、以其他语言进行签名的场景。

```javascript
const crypto = require('crypto');

function computeSignature(privateKeyPem, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${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
);
```

```go
import (
  "crypto/ed25519"
  "crypto/x509"
  "encoding/base64"
  "encoding/pem"
  "errors"
  "fmt"
)

func computeSignature(privateKeyPEM []byte, method, path, timestamp, body string) (string, error) {
  canonical := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body)

  block, _ := pem.Decode(privateKeyPEM)
  if block == nil {
    return "", errors.New("invalid PEM private key")
  }
  key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  if err != nil {
    return "", err
  }
  edKey, ok := key.(ed25519.PrivateKey)
  if !ok {
    return "", errors.New("not an Ed25519 private key")
  }

  // ed25519.Sign hashes internally, sign the canonical string directly
  signature := ed25519.Sign(edKey, []byte(canonical))
  return base64.StdEncoding.EncodeToString(signature), nil
}
```

```python
import time
import base64
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def compute_signature(private_key_pem: bytes, method: str, path: str, timestamp: str, body: str = '') -> str:
  # Build canonical string in fixed order
  canonical = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  private_key = load_pem_private_key(private_key_pem, password=None)
  # .sign() on an Ed25519PrivateKey signs the message directly, no digest argument
  signature = private_key.sign(canonical.encode())

  return base64.b64encode(signature).decode()

# Usage
timestamp = str(int(time.time()))
signature = compute_signature(
  private_key_pem, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
)
```

```php
<?php
// Requires PHP 8.4+ (Ed25519 key support landed in ext-openssl in 8.4)
function computeSignature($privateKeyPem, $method, $path, $timestamp, $body = '') {
  $canonical = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  $privateKey = openssl_pkey_get_private($privateKeyPem);
  // The digest argument is ignored for Ed25519 keys
  openssl_sign($canonical, $signature, $privateKey, 0);

  return base64_encode($signature);
}

// Usage
$timestamp = time();
$signature = computeSignature(
  $privateKeyPem, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  $timestamp
);
```

> **Warning.** 切勿将你的 Ed25519 私钥发送给 Fystack，也不要将其嵌入客户端或浏览器代码中，注册时只需提供公钥。生产环境中，请将密钥保存在 AWS KMS 等硬件级存储中，而不是 PEM 文件中，详见[客户端密钥对](https://docs.fystack.io/api-keys.md#client-key-pair)。

### 通过 AWS KMS 签名（不使用 SDK）

如果你在 Node.js 上使用 `@fystack/sdk`，`AwsKmsSigner` 已经为你完成了这一步，详见[客户端密钥对](https://docs.fystack.io/api-keys.md#client-key-pair)。对于其他所有后端，或者你只是想了解其机制，同样的方法适用于任何语言的 AWS SDK：使用 `MessageType: RAW` 和 `SigningAlgorithm: ED25519_SHA_512` 对原始规范字符串调用 KMS `Sign`，然后对返回的签名字节进行 base64 编码。这正是 `AwsKmsSigner` 的内部实现，下面的 Node.js 示例就是直接写出的相同逻辑。

> **Warning.** `MessageType` 必须为 `RAW`，而不是 `DIGEST`。KMS 的 `ED25519_SHA_512` 算法要求传入未经哈希的消息并在内部进行哈希，这与本页其他部分中 Ed25519 的工作方式一致。传入 `DIGEST` 会对一个不同的、无法互通的值进行签名。

```javascript
import { KMSClient, SignCommand } from '@aws-sdk/client-kms'

async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${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')
}
```

```go
import (
  "context"
  "encoding/base64"
  "fmt"

  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/service/kms"
  "github.com/aws/aws-sdk-go-v2/service/kms/types"
)

func computeSignature(ctx context.Context, client *kms.Client, keyId, method, path, timestamp, body string) (string, error) {
  canonical := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body)

  out, err := client.Sign(ctx, &kms.SignInput{
    KeyId:            aws.String(keyId),
    Message:          []byte(canonical),
    MessageType:      types.MessageTypeRaw,
    SigningAlgorithm: types.SigningAlgorithmSpecEd25519Sha512,
  })
  if err != nil {
    return "", err
  }

  return base64.StdEncoding.EncodeToString(out.Signature), nil
}
```

```java
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.MessageType;
import software.amazon.awssdk.services.kms.model.SignRequest;
import software.amazon.awssdk.services.kms.model.SignResponse;
import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;
import java.util.Base64;

public String computeSignature(KmsClient kmsClient, String keyId, String method, String path, String timestamp, String body) {
  String canonical = String.format("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body);

  SignRequest request = SignRequest.builder()
    .keyId(keyId)
    .message(SdkBytes.fromUtf8String(canonical))
    .messageType(MessageType.RAW)
    .signingAlgorithm(SigningAlgorithmSpec.ED25519_SHA_512)
    .build();

  SignResponse response = kmsClient.sign(request);
  return Base64.getEncoder().encodeToString(response.signature().asByteArray());
}
```

```python
import base64

def compute_signature(kms_client, key_id: str, method: str, path: str, timestamp: str, body: str = '') -> str:
  canonical = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  response = kms_client.sign(
    KeyId=key_id,
    Message=canonical.encode(),
    MessageType='RAW',
    SigningAlgorithm='ED25519_SHA_512'
  )

  return base64.b64encode(response['Signature']).decode()
```

```php
<?php
use Aws\Kms\KmsClient;

function computeSignature(KmsClient $kms, string $keyId, string $method, string $path, string $timestamp, string $body = ''): string {
  $canonical = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  $result = $kms->sign([
    'KeyId' => $keyId,
    'Message' => $canonical,
    'MessageType' => 'RAW',
    'SigningAlgorithm' => 'ED25519_SHA_512',
  ]);

  // $result['Signature'] is already raw bytes, the SDK base64-decodes it for you
  return base64_encode($result['Signature']);
}
```

上面的 `kmsClient`/`client`/`$kms` 就是你所用语言的 AWS SDK 提供的 KMS 客户端，按你平时的方式配置即可。本地与生产环境的通用配置模式请参阅[客户端密钥对](https://docs.fystack.io/api-keys.md#client-key-pair)（本地将其指向 LocalStack/ministack 并使用静态测试凭证，生产环境去掉这两项，由 IAM 角色提供凭证）。无论使用哪种语言，思路都相同，只是通过各 SDK 自身的配置选项来实现。
