> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oristapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 认证与签名

# 认证与签名

接入 OristaPay Open API 采用 **双重认证**：

* **OAuth2 访问令牌** — 证明调用方身份
* **HMAC 请求签名** — 证明请求未被篡改

> 每次请求必须同时通过两层校验，任一失败即返回 `401 Unauthorized`。

## 1. 获取访问令牌

标准 OAuth2 `client_credentials` 流程。

**请求**

```
POST /realms/digitalasset/protocol/openid-connect/token HTTP/1.1
Host: auth.uat.rdezlink.tech
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={api_key}
&client_secret={api_secret}
```

**响应**

```json theme={null}
{
  "access_token": "eyJhbGciOi...",
  "token_type":   "Bearer",
  "expires_in":   300
}
```

**使用约束**

* `access_token` 在 `expires_in` 秒内可复用，**建议缓存**并在到期前 30 秒主动刷新
* `access_token` 必须由与 `X-Api-Key` 对应的 OAuth 客户端签发

<Warning>
  使用 A 商户的 `api_key` 配合 B 商户的 `access_token` 调用会被拒绝。
</Warning>

## 2. 请求签名算法

**待签字符串拼接规则**

```
string_to_sign = METHOD + PATH + TIMESTAMP + NONCE + SHA256_HEX(BODY)
```

**签名计算**

```
signature = HEX( HMAC_SHA256( sign_secret, string_to_sign ) )
```

**字段定义**

| 元素          | 定义                                                          |
| ----------- | ----------------------------------------------------------- |
| `METHOD`    | 请求方法，全大写，如 `POST`                                           |
| `PATH`      | 接口路径，不含域名与 query，如 `/api/v1/wallet/list`                    |
| `TIMESTAMP` | 13 位 UTC 毫秒时间戳字符串，与 `X-Timestamp` 完全一致                      |
| `NONCE`     | 本次请求唯一随机串，与 `X-Nonce` 完全一致，建议 32 位 hex                      |
| `BODY`      | 请求体原始字节；无 body 时为空字符串（`SHA256_HEX("")` = `e3b0c442...b855`） |

**参考实现**

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import hashlib
    import hmac

    def sign(method: str, path: str, ts: str, nonce: str,
             body: str, sign_secret: str) -> str:
        body_hash = hashlib.sha256(body.encode()).hexdigest()
        to_sign   = method + path + ts + nonce + body_hash
        return hmac.new(sign_secret.encode(),
                        to_sign.encode(),
                        hashlib.sha256).hexdigest()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.security.MessageDigest;
    import java.util.HexFormat;

    public static String sign(String method, String path, String ts,
                              String nonce, byte[] body, String signSecret)
                              throws Exception {
        String bodyHash = HexFormat.of().formatHex(
            MessageDigest.getInstance("SHA-256").digest(body));
        String toSign = method + path + ts + nonce + bodyHash;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(signSecret.getBytes(), "HmacSHA256"));
        return HexFormat.of().formatHex(mac.doFinal(toSign.getBytes()));
    }
    ```
  </Tab>

  <Tab title="Node.js">
    ```javascript theme={null}
    const crypto = require('crypto');

    function sign(method, path, ts, nonce, body, signSecret) {
      const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
      const toSign = method + path + ts + nonce + bodyHash;
      return crypto.createHmac('sha256', signSecret)
                   .update(toSign).digest('hex');
    }
    ```
  </Tab>
</Tabs>

## 3. 请求头规范

所有业务请求必须同时携带以下请求头：

| Header          | 必选 | 说明                                   |
| --------------- | -- | ------------------------------------ |
| `Authorization` | ✓  | `Bearer {access_token}`              |
| `X-Api-Key`     | ✓  | 接入方 `api_key`                        |
| `X-Timestamp`   | ✓  | UTC 毫秒时间戳，允许 **±5 分钟** 偏差            |
| `X-Nonce`       | ✓  | 本次请求唯一随机串，**5 分钟内同 `api_key` 下不可复用** |
| `X-Signature`   | ✓  | §2 计算出的签名                            |
| `Content-Type`  | ✓  | 固定 `application/json; charset=utf-8` |

# 请求与响应约定

**协议规范**

| 项目    | 规范                     |
| ----- | ---------------------- |
| 传输协议  | HTTPS (TLS 1.2+)       |
| 请求方法  | `POST`                 |
| 字符集   | UTF-8                  |
| 请求/响应 | JSON                   |
| 字段命名  | `camelCase`            |
| 时间戳   | 毫秒；请求侧字符串/数字皆可，响应侧统一数字 |

## 响应信封

所有响应均为**单层** JSON `{ code, message, data? }`，由下游业务服务返回，网关不再做二次包装。

**成功（业务正常）**

业务码 `code = 1`，业务数据在 `data` 中：

```json theme={null}
{
  "code":    1,
  "message": "Success",
  "data":    { /* 业务数据；列表接口为数组 */ }
}
```

**业务/下游错误**

下游返回业务错误，HTTP 状态码仍为 `200`，错误细节由业务码 `code` / `message` 表达：

```json theme={null}
{
  "code":    <非 1 整数>,
  "message": "<失败原因>"
}
```

下游 gRPC 不可达 / 超时等场景，HTTP 状态码仍为 `200`，`code` 为负整数（与 gRPC StatusCode 取负，如 `-14` 表示 `UNAVAILABLE`），`message` 给出连接错误详情。

**网关层错误**

路由未命中、网关内部异常、descriptor 缺失等。HTTP 状态码使用对应错误码（如 `400` / `404` / `500` / `502`），响应体仍为单层 `{code, message}`：

```json theme={null}
{
  "code": 404,
  "message": "route not found"
}
```

<Note>
  * HTTP 状态码反映**传输层**处理结果：`2xx` 表示请求成功送达下游并完成转换；非 `2xx` 表示网关侧错误
  * 业务码 `code` 反映**业务层**处理结果：`1` 表示业务成功，`data` 承载返回值；其他值为业务错误码，`message` 给出原因描述
  * **判断成功的正确姿势**：HTTP `2xx` + 业务 `code == 1`，两者皆满足才表示业务调用成功
  * **认证类错误**：`Authorization` / `X-Signature` / `X-Timestamp` / `X-Nonce` 任一校验失败统一返回 HTTP `401`，响应体为 `{"code":401,"message":"Unauthorized"}`
</Note>

# 错误处理

## HTTP 状态码与业务码

响应体统一为单层 `{code, message, data?}`。HTTP 状态码反映网关/传输层结果，业务码 `code` 反映业务处理结果。

| HTTP Status         | 含义              | 响应体                                    | 建议                 |
| ------------------- | --------------- | -------------------------------------- | ------------------ |
| `200` + `code == 1` | 业务成功            | `{code:1, message, data}`              | 从 `data` 取业务字段     |
| `200` + `code != 1` | 下游业务错误          | `{code, message}`                      | 按 `message` 处理业务错误 |
| `200` + `code < 0`  | 请求送达后 gRPC 异常   | `{code, message}`（`code` 为 gRPC 状态码取负） | 检查下游可用性，必要时重试      |
| `400`               | 请求体或参数非法        | `{code, message}`                      | 修正后重试              |
| `401`               | 认证失败（OAuth 或签名） | `{code:401, message}`                  | 按下方排查清单逐项检查        |
| `404`               | 接口不存在           | `{code, message}`                      | 核对路径               |
| `429`               | 触发限流            | `{code, message, ...}`                 | 退避后重试              |
| `5xx`               | 网关或下游服务异常       | `{code, message}`                      | 指数退避后重试            |

## 认证失败排查清单

所有认证类错误统一返回：

```json theme={null}
{ "code": 401, "message": "Unauthorized" }
```

请按以下顺序排查：

1. **Token 是否有效** — 是否过期？是否由当前 `api_key` 签发？
2. **签名是否匹配** — `METHOD / PATH / BODY` 是否与实际发送一致？
3. **时间戳是否在窗口内** — 本机时钟是否与 NTP 同步？
4. **Nonce 是否唯一** — 5 分钟内同一 `api_key` 下不得复用

# 限流策略

| 项    | 值                |
| ---- | ---------------- |
| 维度   | 按 `api_key` 独立计量 |
| 默认配额 | **600 次 / 分钟**   |
| 超限响应 | HTTP `429`       |

**超限响应示例**

```json theme={null}
{
  "code":      429,
  "message":   "rate limit exceeded",
  "limit":     600,
  "window_ms": 60000
}
```

> 需要更高配额请联系商务，调整后次分钟生效。

# 安全建议

| 主题   | 最佳实践                                                      |
| ---- | --------------------------------------------------------- |
| 密钥管理 | `api_secret` 与 `sign_secret` 仅在服务端使用，**严禁下发到前端或移动端**      |
| 凭证轮换 | 定期轮换；发现泄露立即联系商务重置                                         |
| 传输安全 | 强制 HTTPS，拒绝任何未启用 TLS 1.2+ 的客户端                            |
| 时钟同步 | 使用 NTP 保持 ±1 分钟内的精度，避免时钟漂移引发误判                            |
| 日志脱敏 | 应用日志中不得保存完整 `api_secret` / `sign_secret` / `access_token` |
