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

# 签名验签说明

## 机制概述

开放平台采用 **app\_key 对称签名**机制：渠道商与平台共享一对凭据，使用 **HMAC-SHA256** 对请求/响应原文计算签名，通过 HTTP Header 传递。

* **仅签名，不加密**：报文内容以 HTTPS 传输保护；签名用于确认请求来源可信、内容未被篡改、防止重放攻击；
* **对称机制**：同一密钥既用于请求签名（渠道商→平台），也用于响应验签（平台→渠道商）；
* **防重放**：时间戳 ±5 分钟窗口 + 随机串 5 分钟去重，双因子校验。

## 凭据获取

渠道商入驻（管理端开通）后，平台分配：

| 凭据           | 说明                                                         |
| ------------ | ---------------------------------------------------------- |
| `app_key`    | 32 位十六进制字符串，即请求头 `Client-Id`，用于平台定位渠道商身份与密钥                |
| `app_secret` | HMAC 密钥，与 `app_key` 配对使用，**务必妥善保管，严禁泄露**（不要写入代码仓库、日志、聊天记录） |

<Note>
  请求头 `Client-Id` 的取值即为 `app_key` 本身。
</Note>

## 请求签名（渠道商 → 平台）

### 签名原文

对以下内容按顺序拼接，**每行以换行符 `\n` 结尾**（包括最后一行的请求体后也有 `\n`）：

```
HTTP方法\n
URL\n
Client-Id\n
Signature-Type\n
Timestamp\n
Nonce-Str\n
请求体\n
```

对应格式串：`"%s\n%s\n%s\n%s\n%s\n%s\n%s\n"`。

| 行 | 内容             | 说明                                                                               |
| - | -------------- | -------------------------------------------------------------------------------- |
| 1 | HTTP 方法        | 全部接口为 `POST`                                                                     |
| 2 | URL            | **完整请求 URL**，包含协议、域名、路径与查询参数原文（如 `https://gw.xft.xin/openapi/v1/merchant/apply`） |
| 3 | Client-Id      | 请求头 `Client-Id` 的值，即 app\_key                                                    |
| 4 | Signature-Type | 固定 `HmacSHA256`                                                                  |
| 5 | Timestamp      | 请求头 `Timestamp` 的值，秒级时间戳                                                         |
| 6 | Nonce-Str      | 请求头 `Nonce-Str` 的值                                                               |
| 7 | 请求体            | **发送的原始请求体字节**（对实际发送的 JSON 原文签名，不做任何格式化）                                         |

### 签名算法

```
Signature = hex( HMAC_SHA256( app_secret, 签名原文 ) )
```

* 算法：HMAC-SHA256，密钥为 `app_secret`；
* 输出：十六进制小写字符串，放入请求头 `Signature`。

### 计算示例

以进件提交接口为例，假设：

| 项                   | 值                                                                                  |
| ------------------- | ---------------------------------------------------------------------------------- |
| 方法                  | `POST`                                                                             |
| URL                 | `https://gw.xft.xin/openapi/v1/merchant/apply`                                     |
| app\_key（Client-Id） | `a1b2c3d4e5f67890a1b2c3d4e5f67890`                                                 |
| app\_secret         | `f6e5d4c3b2a19087f6e5d4c3b2a19087`                                                 |
| Signature-Type      | `HmacSHA256`                                                                       |
| Timestamp           | `1755234000`                                                                       |
| Nonce-Str           | `9f8e7d6c5b4a39281706f5e4d3c2b1a0`                                                 |
| 请求体                 | `{"out_apply_no":"P20260815001","back_url":"https://merchant.example.com/notify"}` |

签名原文（第 7 行之后也有换行符）：

```
POST\n
https://gw.xft.xin/openapi/v1/merchant/apply\n
a1b2c3d4e5f67890a1b2c3d4e5f67890\n
HmacSHA256\n
1755234000\n
9f8e7d6c5b4a39281706f5e4d3c2b1a0\n
{"out_apply_no":"P20260815001","back_url":"https://merchant.example.com/notify"}\n
```

Go 计算参考：

```go theme={null}
raw := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
    "POST",
    "https://gw.xft.xin/openapi/v1/merchant/apply",
    "<app_key>",          // Client-Id
    "HmacSHA256",
    "1755234000",         // Timestamp
    "<nonce_str>",        // Nonce-Str
    `{"out_apply_no":"P20260815001","back_url":"https://merchant.example.com/notify"}`, // 请求体原文
)
mac := hmac.New(sha256.New, []byte("<app_secret>"))
mac.Write([]byte(raw))
signature := hex.EncodeToString(mac.Sum(nil)) // 放入请求头 Signature
```

完整请求：

```bash theme={null}
curl -X POST https://gw.xft.xin/openapi/v1/merchant/apply \
  -H "Content-Type: application/json" \
  -H "Client-Id: a1b2c3d4e5f67890a1b2c3d4e5f67890" \
  -H "Signature-Type: HmacSHA256" \
  -H "Timestamp: 1755234000" \
  -H "Nonce-Str: 9f8e7d6c5b4a39281706f5e4d3c2b1a0" \
  -H "Signature: <计算出的签名值>" \
  -d '{"out_apply_no":"P20260815001","back_url":"https://merchant.example.com/notify"}'
```

## 平台验签流程

平台收到请求后依次校验，任何一步失败即拒绝：

| 步骤 | 校验                                              | 失败表现               |
| -- | ----------------------------------------------- | ------------------ |
| 1  | 必须携带 `Timestamp` 与 `Nonce-Str`（缺失直接拒绝，防止绕过重放防护） | HTTP 400           |
| 2  | 时间戳为合法秒级时间戳，且与平台时间差 ≤ 5 分钟                      | HTTP 401 `NO_AUTH` |
| 3  | `Nonce-Str` 在 5 分钟窗口内未重复使用（按 app\_key 去重）       | HTTP 401 `NO_AUTH` |
| 4  | 按上文算法计算签名，与请求头 `Signature` 常量时间比对               | HTTP 401 `NO_AUTH` |

## 响应验签（平台 → 渠道商，可选但推荐）

平台对**所有响应均签名**（包括 4xx/5xx 错误响应），签名放入响应头 `Signature`：

```
响应签名原文 = HTTP方法\n URL\n Client-Id\n Signature-Type\n Timestamp\n Nonce-Str\n 响应体\n
```

**复用请求的头部参数**（`Client-Id`/`Signature-Type`/`Timestamp`/`Nonce-Str` 与请求一致），仅最后一行替换为响应 body 原文，同样以 `\n` 结尾。

渠道商可用同一 `app_secret` 对响应验签，确认响应未被篡改。

## 防重放说明

* `Timestamp`：限制请求在 5 分钟窗口内有效，过期请求拒绝（HTTP 401）；
* `Nonce-Str`：同一 app\_key 下 5 分钟窗口内不可重复，防止同一请求重放；
* 两者同时校验，缺一不可。

## 注意事项

1. 签名前**不要格式化请求体**（不要重新排序 JSON 字段、不要压缩/美化），对实际发送的字节签名；
2. URL 必须包含完整协议与域名（`https://gw.xft.xin/openapi/v1/...`），且与实际请求完全一致（含 query 参数原序）；
3. 时间戳使用秒级；服务器时间与平台时间偏差过大时先校准系统时钟；
4. `app_secret` 泄露时立即联系平台重置；
5. 对接期间可联系平台将接口加入验签豁免名单（仅限联调）。

## 与 RSA 签名机制差异

若渠道商同时对接其他支付机构（如易生 RSA 双向签名），注意以下差异：

| 项    | X.PAM（app\_key）                           | 易生/易宝（RSA）              |
| ---- | ----------------------------------------- | ----------------------- |
| 密钥体系 | 对称（共享 `app_secret`）                       | 非对称（私钥签名 / 公钥验签）        |
| 签名位置 | HTTP Header（`Signature`）                  | 报文字段（reqSign / sign）    |
| 签名原文 | `方法\nURL\nClientId\n类型\n时间戳\n随机串\nBody\n` | ASCII 升序参数 + MD5 摘要     |
| 算法   | HMAC-SHA256，输出 hex                        | SHA256withRSA，输出 base64 |
| 响应   | 响应头签名（含错误响应）                              | 独立应答签名                  |
