Files
soul-yongping/open-api-sign.md

239 lines
7.7 KiB
Markdown
Raw Normal View History

2026-03-14 14:37:17 +08:00
# 存客宝开放 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`)对应 |