# 存客宝开放 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 ` | 本文档规范,需签名与加密 | 当前 Soul 项目「链接卡若」使用的是**内部 API**,与本文档的开放 API 不同。 --- ## 一、整体流程 ``` 第一步 POST /v1/open/auth/token 携带:apiKey + account + timestamp + sign 服务端验签后返回 JWT Token(有效期 2 小时) │ ▼ 第二步 POST /v1/open/scenarios (或其他业务接口) Header: Authorization: Bearer 无需再传 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 ``` 不再需要 `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`)对应 |