---
url: /guide/authentication.md
---
# 签名认证

所有 `/v1/**` 接口都用 HMAC-SHA256 签名认证，没有登录态、没有 token。

## 请求头

| 请求头 | 说明 |
|---|---|
| `X-Api-Key` | API key |
| `X-Timestamp` | 当前时间，Unix **毫秒**。与服务器相差超过 ±30 秒拒绝（`REQUEST_EXPIRED`） |
| `X-Nonce` | 16–64 位随机串，同一个 key 5 分钟内不能重复（`NONCE_REPLAYED`）。建议 16 字节随机数的 hex |
| `X-Signature` | 签名，见下文，hex 小写 |
| `Content-Type` | 有请求体时为 `application/json` |

## 待签名串

用 `\n`（换行，0x0A）把 6 段连起来，**末尾没有换行**：

```text
METHOD
PATH
CANONICAL_QUERY
TIMESTAMP
NONCE
hex(sha256(BODY))
```

| 段 | 规则 |
|---|---|
| `METHOD` | 大写，如 `GET`、`POST` |
| `PATH` | URL 路径，不含域名和查询串，按实际发送的（已百分号编码的）形式，如 `/v1/wallets/u_10001/addresses` |
| `CANONICAL_QUERY` | 查询参数规范化：按 key 排序，同 key 按 value 排序；key 与 value 按 RFC 3986 编码（只保留 `A-Z a-z 0-9 - _ . ~`，空格编码为 `%20` 而不是 `+`）；用 `&` 连接。没有查询参数时为空串 |
| `TIMESTAMP` | 与 `X-Timestamp` 相同 |
| `NONCE` | 与 `X-Nonce` 相同 |
| `hex(sha256(BODY))` | 请求体原始字节的 SHA-256，hex 小写。没有请求体（如 GET）时是空串的哈希 `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |

```text
X-Signature = hex(HMAC-SHA256(API secret, 待签名串))
```

::: warning 两个最常见的坑

1. **签的 body 必须就是发出去的字节**。先把 JSON 序列化成字符串，用这个字符串算哈希并作为请求体发送；不要签一份、HTTP 库再序列化一份。
2. **直接把规范化后的查询串作为 URL 的查询部分发送**，就不会出现「签的和发的顺序 / 编码不一致」。
   :::

## 示例

API secret 为 `test-secret`，`X-Timestamp: 1762650000000`，`X-Nonce: 0123456789abcdef0123`：

```http
GET /v1/deposits?user_id=u%201&chain_code=TRON&status=PENDING&status=CONFIRMED&note=a%2Bb%21
```

规范化查询串：

```text
chain_code=TRON&note=a%2Bb%21&status=CONFIRMED&status=PENDING&user_id=u%201
```

待签名串：

```text
GET
/v1/deposits
chain_code=TRON&note=a%2Bb%21&status=CONFIRMED&status=PENDING&user_id=u%201
1762650000000
0123456789abcdef0123
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

签名：`d474bb4c8ff89a37f214d8c39c3d17a502d54b64c1822024a86480c248bcbc86`

带请求体（`POST /v1/withdraws`，同样的时间戳和 nonce）：

```json
{"user_id":"u1","chain_code":"TRON","token_code":"USDT","to_address":"TXYZ","amount":"12.5","biz_id":"order-1"}
```

签名：`381b9e20fc9b2e0cd97ecb003686acd472db596757b674e56df9e038d708867c`

用这两组数据可以校验自己的实现。

## 权限范围与 IP 白名单

每个 API key 有权限范围，接口需要的范围写在 [接口参考](/reference/) 里：

| 范围 | 能做什么 |
|---|---|
| `read` | 所有查询接口、重发事件 |
| `deposit` | 创建充值地址、补单 |
| `withdraw` | 发起提现 |

缺少范围返回 `PERMISSION_DENIED`；请求来源 IP 不在 key 的白名单里返回 `IP_NOT_ALLOWED`。建议发起提现用单独的 `withdraw` key，并只放行服务器出口 IP。

## 鉴权错误

| reason | HTTP | 原因 |
|---|---|---|
| `MERCHANT_AUTH_FAILED` | 401 | 缺少签名请求头、key 不存在或已停用、商户已停用 |
| `SIGNATURE_INVALID` | 401 | 签名不对：对照上面的示例逐段检查待签名串 |
| `REQUEST_EXPIRED` | 401 | 时间戳缺失、不是毫秒、或与服务器相差超过 30 秒（检查服务器时钟 / NTP） |
| `NONCE_REPLAYED` | 401 | nonce 重复，或长度不在 16–64 |
| `IP_NOT_ALLOWED` | 403 | 来源 IP 不在白名单 |
| `PERMISSION_DENIED` | 403 | key 没有接口需要的权限范围 |
