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

7.7 KiB
Raw Blame History

存客宝开放 API — 鉴权规范V1

适用接口:/v1/open/*


〇、存客宝 API 区分说明

存客宝存在两套 API,互不通用:

类型 路径 鉴权方式 使用场景
内部 API /v1/api/scenarios apiKey + timestamp + signquery 传参),无需 JWT 小程序首页「链接卡若」、文章 @ 人物留资等
开放 API /v1/open/* POST /v1/open/auth/token 获取 JWTAuthorization: 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 < timestampASCII 升序),所以拼接顺序固定为: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>

不再需要 apiKeysigntimestamp 参数。

Token 过期2 小时)后重新请求 /v1/open/auth/token 换新 Token。


五、示例代码

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

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)对应