239 lines
7.7 KiB
Markdown
239 lines
7.7 KiB
Markdown
|
|
# 存客宝开放 API — 鉴权规范(V1)
|
|||
|
|
|
|||
|
|
> 适用接口:`/v1/open/*`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 〇、存客宝 API 区分说明
|
|||
|
|
|
|||
|
|
存客宝存在**两套 API**,互不通用:
|
|||
|
|
|
|||
|
|
| 类型 | 路径 | 鉴权方式 | 使用场景 |
|
|||
|
|
|------|------|----------|----------|
|
|||
|
|
| **内部 API** | `/v1/api/scenarios` | apiKey + timestamp + sign(query 传参),**无需 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 < timestamp(ASCII)
|
|||
|
|
$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`)对应 |
|