7.7 KiB
7.7 KiB
存客宝开放 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值
成功响应
{
"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
$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
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
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)对应 |