Files
soul-yongping/open-api-sign.md
2026-03-14 14:37:17 +08:00

239 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 存客宝开放 API — 鉴权规范V1
> 适用接口:`/v1/open/*`
---
## 〇、存客宝 API 区分说明
存客宝存在**两套 API**,互不通用:
| 类型 | 路径 | 鉴权方式 | 使用场景 |
|------|------|----------|----------|
| **内部 API** | `/v1/api/scenarios` | apiKey + timestamp + signquery 传参),**无需 JWT** | 小程序首页「链接卡若」、文章 @ 人物留资等 |
| **开放 API** | `/v1/open/*` | 先 `POST /v1/open/auth/token` 获取 JWT`Authorization: Bearer <token>` | 本文档规范,需签名与加密 |
当前 Soul 项目「链接卡若」使用的是**内部 API**,与本文档的开放 API 不同。
---
## 一、整体流程
```
第一步 POST /v1/open/auth/token
携带apiKey + account + timestamp + sign
服务端验签后返回 JWT Token有效期 2 小时)
第二步 POST /v1/open/scenarios (或其他业务接口)
Header: Authorization: Bearer <token>
无需再传 apiKey / sign与存客宝内部接口完全兼容
```
---
## 二、API Key 说明
| 属性 | 描述 |
|--------------|----------------------------------------------------------------|
| **颁发对象** | 每个存客宝账号(`ck_users`)一把专属 API Key |
| **格式** | `5 组 × 5 位`,大小写字母 + 数字,组间以 `-` 分隔 |
| **示例** | `aB3k9-Z8c1Q-0f4Xk-M9n2P-1A2b3` |
| **获取方式** | 门店端 → 用户中心 → 对外接口 → 查看/生成 API Key |
| **有效期** | 永久有效,可随时点击"重新生成"覆盖旧 Key旧 Key 立即失效) |
---
## 三、第一步:获取 JWT Token
### 接口
```
POST /v1/open/auth/token
Content-Type: application/json
```
### 请求参数
| 字段 | 类型 | 必填 | 说明 |
|-------------|--------|------|---------------------------------------------------|
| `apiKey` | string | 是 | 账号专属 API Key |
| `account` | string | 是 | 登录账号(`ck_users.account` |
| `timestamp` | int | 是 | 当前秒级 Unix 时间戳(与服务器时差不超过 5 分钟)|
| `sign` | string | 是 | 签名值,生成方式见下方 |
### 签名算法
只有三个固定字段参与签名,业务参数不参与:
```
stringToSign = account值 + timestamp值 ← 字段名 ASCII 升序,直接拼接值
firstMd5 = MD5(stringToSign)
sign = MD5(firstMd5 + apiKey)
```
> `account` < `timestamp`ASCII 升序),所以拼接顺序固定为:`account值 + timestamp值`
### 成功响应
```json
{
"code": 200,
"message": "success",
"data": {
"token": "eyJ...",
"expires_in": 7200
}
}
```
### 常见错误
| code | message | 原因 |
|------|-----------------|----------------------------------------|
| 400 | apiKey不能为空 | 未传 `apiKey` |
| 400 | account不能为空 | 未传 `account` |
| 400 | sign不能为空 | 未传 `sign` |
| 400 | timestamp不能为空 | 未传 `timestamp` |
| 400 | 请求已过期 | `timestamp` 与服务器时差超 5 分钟 |
| 401 | 无效的apiKey | Key 不存在、账号不匹配或账号已禁用 |
| 401 | 签名验证失败 | account / timestamp / apiKey 值有误 |
---
## 四、第二步:调用业务接口
拿到 Token 后,所有 `/v1/open/*` 接口在 HTTP Header 中携带:
```
Authorization: Bearer <token>
```
不再需要 `apiKey``sign``timestamp` 参数。
Token 过期2 小时)后重新请求 `/v1/open/auth/token` 换新 Token。
---
## 五、示例代码
### PHP
```php
$apiKey = 'aB3k9-Z8c1Q-0f4Xk-M9n2P-1A2b3';
$account = 'user001';
$timestamp = (string)time();
// 生成签名
$stringToSign = $account . $timestamp; // account < timestampASCII
$firstMd5 = md5($stringToSign);
$sign = md5($firstMd5 . $apiKey);
// 获取 Token
$response = file_get_contents('https://ckbapi.quwanzhi.com/v1/open/auth/token', false,
stream_context_create(['http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode(compact('apiKey', 'account', 'timestamp', 'sign')),
]])
);
$token = json_decode($response, true)['data']['token'];
// 调用业务接口
$response2 = file_get_contents('https://ckbapi.quwanzhi.com/v1/open/scenarios', false,
stream_context_create(['http' => [
'method' => 'POST',
'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json",
'content' => json_encode([
'planId' => 42,
'phone' => '13800000000',
'name' => '张三',
'source' => '百度推广',
]),
]])
);
```
### JavaScript / Node.js
```javascript
const crypto = require('crypto');
const apiKey = 'aB3k9-Z8c1Q-0f4Xk-M9n2P-1A2b3';
const account = 'user001';
const timestamp = String(Math.floor(Date.now() / 1000));
// 签名
const firstMd5 = crypto.createHash('md5').update(account + timestamp, 'utf8').digest('hex');
const sign = crypto.createHash('md5').update(firstMd5 + apiKey, 'utf8').digest('hex');
// 获取 Token
const res = await fetch('https://ckbapi.quwanzhi.com/v1/open/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey, account, timestamp, sign }),
});
const { token } = (await res.json()).data;
// 调用业务接口
const res2 = await fetch('https://ckbapi.quwanzhi.com/v1/open/scenarios', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ planId: 42, phone: '13800000000', name: '张三' }),
});
```
### Python
```python
import hashlib, time, requests
api_key = 'aB3k9-Z8c1Q-0f4Xk-M9n2P-1A2b3'
account = 'user001'
timestamp = str(int(time.time()))
# 签名
first_md5 = hashlib.md5((account + timestamp).encode()).hexdigest()
sign = hashlib.md5((first_md5 + api_key).encode()).hexdigest()
# 获取 Token
r = requests.post('https://ckbapi.quwanzhi.com/v1/open/auth/token',
json={'apiKey': api_key, 'account': account,
'timestamp': timestamp, 'sign': sign})
token = r.json()['data']['token']
# 调用业务接口
r2 = requests.post('https://ckbapi.quwanzhi.com/v1/open/scenarios',
headers={'Authorization': f'Bearer {token}'},
json={'planId': 42, 'phone': '13800000000', 'name': '张三'})
```
---
## 六、签名自测用例
| 字段 | 值 |
|-----------|------------------------------------|
| apiKey | `TestKey-12345-ABCDE-67890-xYzWv` |
| account | `user001` |
| timestamp | `1710000000` |
计算过程:
```
stringToSign = "user001" + "1710000000" = "user0011710000000"
firstMd5 = MD5("user0011710000000")
sign = MD5(firstMd5 + "TestKey-12345-ABCDE-67890-xYzWv")
```
---
## 七、项目配置(当前密钥)
> ⚠️ **安全提醒**:本文件含真实密钥,请勿推送到公共仓库。建议将 `open-api-sign.md` 加入 `.gitignore`,或改用环境变量 `CKB_API_KEY` 存储。
| 字段 | 值 |
|--------|--------------------------------------|
| apiKey | `mI9Ol-NO6cS-ho3Py-7Pj22-WyK3A` |
| account| 需与存客宝账号(`ck_users.account`)对应 |