API 接口文档
所有客户端接口均走 AES-256-GCM 加密通道 + HMAC-SHA256 签名 + 防重放,未持有 appSecret 无法伪造、篡改或重放任何请求。本文档与 docs/API.md 同源。
01安全协议
1.1 密钥派生
encKey = SHA256(appSecret + ":enc") // 32 字节,用于 AES-256-GCM macKey = SHA256(appSecret + ":mac") // 32 字节,用于 HMAC 签名
1.2 请求报文(POST 接口)
明文 JSON 经 AES-256-GCM 加密后作为唯一 body 字段发送:
{ "e": "<base64( iv[12] | 密文 | tag[16] )>" }
- iv 为每请求随机的 12 字节
- 解密失败/被篡改(GCM 认证失败)一律拒绝,不泄露细节
1.3 请求头
| 头 | 说明 |
|---|---|
X-App-Key | 应用的 appKey |
X-Timestamp | 当前 Unix 秒(允许偏差 ±300s,可配置) |
X-Nonce | 随机 32 位 hex,5 分钟内唯一(防重放) |
X-Sign | hex( HMAC-SHA256(e + timestamp + nonce, macKey) ) |
X-Proto-Ver | 可选。协议版本协商,目前唯一有效值 2(见 1.5 响应加密) |
GET /config、GET /events 无加密报文,签名时 e 取空串。
1.4 响应结构(明文模式,默认)
{ "code": 0, "msg": "success", "data": { ... } }
HTTP 一律 200,业务结果看 code。
1.5 响应加密协商(X-Proto-Ver: 2)
当同时满足以下两个条件时,服务端把最终响应 {code, msg, data} 整体加密返回:
- 请求携带头
X-Proto-Ver: 2 - 应用在管理端开启了 响应加密(encryptResponse) 开关
加密响应结构(与请求报文同一协议):
{ "e": "<base64( iv[12] | 密文 | tag[16] )>" }
客户端用 encKey = SHA256(appSecret + ":enc") 以 AES-256-GCM 解密即得 {code, msg, data}。
X-Proto-Ver: 2 的请求始终返回明文,保证老 SDK 兼容;异常路径(业务错误)目前仍返回明文 {code, msg, data},SDK 应先探测顶层是否为 {e} 再决定解密。
1.6 业务码
| code | 含义 |
|---|---|
| 0 | 成功 |
| 1001 | 签名错误 / 解密失败 |
| 1002 | 时间戳超差 |
| 1003 | 请求重放(nonce 重复) |
| 1004 | 触发限流或暴力破解锁定 |
| 2001 | 应用不存在或已停用 |
| 3001 | 卡密不存在 |
| 3002 | 卡密已使用/已停用 |
| 4001 | 用户名或密码错误 |
| 4002 | 账号已停用 |
| 4003 | 账号已到期 |
| 4004 | token 无效或已失效 |
| 4005 | 账号在其他设备登录(被顶下线) |
| 4006 | 用户名已存在 |
| 4007 | 设备数超限(机器绑定) |
| 4008 | 客户端哈希不在白名单 |
| 4009 | 云变量/云函数已冻结 |
| 4010 | 云变量/云函数不存在 |
| 5000 | 服务器内部错误 |
02客户端接口列表(/api/v1)
{ "cardKey": "XXXX-XXXX-XXXX-XXXX-XXXX", "username": "user1", "password": "pass123456" }
- 用户名不存在 → 注册新账号,到期时间 = 当前时间 + 卡密时长
- 用户名已存在 → 续期(到期时间往后累加卡密时长)
- 卡密兑换后标记为已使用
- 防爆破:同一应用+用户名连续兑换失败(卡密已使用/已停用)5 次,锁定 15 分钟(返回 1004)
{ "username": "user1", "expireAt": "<ISO8601>" }
{
"username": "user1",
"password": "pass123456",
"clientVer": "1.0.0",
"machineCode": "<可选,机器码,最长 64>",
"clientHash": "<可选,客户端哈希,最长 64>",
"deviceType": "<可选,pc / mobile / other,缺省按 other>"
}
{
"token": "<会话token>",
"username": "user1",
"expireAt": "2026-09-15T12:00:00.000Z",
"announcement": "公告内容",
"latestVersion": "1.2.0",
"updateUrl": "https://...",
"forceUpdate": false,
"heartbeatIntervalSec": 60
}
- 应用开启「单点在线」时,新登录会顶掉旧会话(旧端下次心跳收到 4005;被顶信息不再通过事件下发,见第 3 章)
- 机器绑定(machineBind):开启后登录必须携带
machineCode(未携带返回 4007 并提示升级客户端)。同一用户最多绑定maxDevices台设备;已有设备登录刷新活跃时间,新设备超限返回 4007。管理端可随时在「用户详情-设备」里解绑某台设备 - 客户端哈希白名单(requireClientHash):开启后登录必须携带
clientHash,且须命中管理端配置的allowedClientHashes(换行分隔的 SHA256 白名单),否则返回 4008 - 老 SDK 不传以上可选字段时:仅在应用开启对应开关时才受影响,否则行为不变
Authorization: Bearer <token>(无需签名)。{ "machineCode": "<可选,机器码>" }
老 SDK 发 {} 或空 body 不受影响。当应用开启机器绑定、会话已绑定机器码、且请求携带的 machineCode 与会话不一致时,返回 4004。
{
"serverTime": "...",
"expireAt": "...",
"remainSec": 3540,
"events": [ { "type": "announcement", "payload": "...", "createdAt": "..." } ]
}
events为该会话待送达事件(最多 20 条/次,无事件为空数组),送达即消费,不会重复下发,详见第 3 章- 客户端应按
heartbeatIntervalSec(服务端建议值)周期调用 - 超过应用配置的心跳超时未心跳,会话被清扫器删除,下次心跳返回 4004
Authorization: Bearer <token>。{ "announcement": "...", "latestVersion": "...", "updateUrl": "...", "forceUpdate": false }
e 取空串)+ Authorization: Bearer <token>。{ "events": [ { "type": "...", "payload": "...", "createdAt": "..." } ] }
与心跳下发同源:返回即标记已消费,二次拉取不会重复返回。供不方便等心跳周期的场景主动拉取。
{ "name": "license_flags" }
{ "value": "<管理端配置的变量值>" }
mode = 0(免登录):仅需签名通道mode = 1(需 Token):另需Authorization: Bearer <token>,token 无效返回 4004- 变量不存在返回 4010;被管理员冻结返回 4009
{ "name": "sign_license", "payload": "<可选,最长 8192 的字符串>" }
| type | 行为 | 返回 data |
|---|---|---|
HTTP_PROXY | 服务端代发 GET/POST 到 targetUrl(含 SSRF 防护,禁内网/重定向) | { "value": "<上游响应体,截断 64KB>", "status": 200 } |
HMAC_SIGN | 服务端用私有 secret 计算 hex(HMAC-SHA256(payload + timestamp, secret)) | { "value": "<64hex>", "timestamp": 1755420000 } |
CHALLENGE | 生成一次性挑战值(TTL 5 分钟,消费后失效) | { "value": "<64hex(32 字节随机)>" } |
mode鉴权与错误码同云变量(mode=1 需 token;4009 冻结 / 4010 不存在)HMAC_SIGN的 secret 由管理端配置(为空时用 appSecret 派生 macKey),客户端永远拿不到密钥
03跨端事件同步
同一账号多端登录(手机/电脑)时,服务端通过 心跳顺带推送 + 拉取端点 同步状态,无需长连接。
3.1 事件类型
| type | 触发方 | payload | 说明 |
|---|---|---|---|
announcement | 管理端推送 | 公告文本 | 同时不改变 app.announcement 字段本身 |
custom | 管理端推送 | 自定义消息文本 | 管理员自定义消息 |
expiring | 心跳自动检测 | JSON 串 {expireAt} | 账号距到期 ≤24h 时自动生成(未送达期间不重复生成) |
3.2 下发规则
- 事件按用户维度存储;
sessionToken为空推给该用户所有会话,非空仅推给 token 匹配的会话(管理端定向推送用) - 每次心跳/拉取最多下发 20 条,按创建时间升序
- 送达即标记已消费(deliveredAt),不会重复下发(并发/重试下同一事件只会被一个请求返回;若响应丢失亦不重发);已送达事件保留 7 天后由清扫器删除
04管理端 API 摘要(/admin/v1)
以下接口均需 Bearer 管理端 JWT。统一返回 {code, msg, data};code 非 0 时 HTTP 状态码同业务语义(4xx)。
4.1 认证
{token, username}。连续失败 5 次锁定 15 分钟(返回 401)。4.2 应用(apps)
/admin/v1/apps/:id 删除、POST /admin/v1/apps/:id/regen-secret 重置密钥。- 基础:
name / latestVersion / updateUrl / forceUpdate / announcement / heartbeatTimeoutSec / singleOnline / status - 安全配置:machineBind(bool)、maxDevices(1-100)、requireClientHash(bool)、allowedClientHashes(换行分隔 SHA256 白名单)、encryptResponse(bool,响应加密开关)
{ "usernames": ["user1"], "type": "announcement", "message": "维护通知" }
usernames 缺省 = 推给该应用全部活跃用户;type 仅 announcement / custom。返回 { "pushed": <人数> }。事件由客户端心跳或 GET /api/v1/events 消费。
4.3 卡密(cards)
{ "appId": 1, "count": 100, "durationMinutes": 43200, "batchNo": "<可选>", "remark": "<可选>" }
count 1-1000;durationMinutes ≥ 1。
page / pageSize / appId / status / batchNo / keyword 筛选。appId / status / batchNo 筛选。4.4 用户与设备(users / devices)
/admin/v1/users/:id 删除用户。adjust-expiry(到期时间调整)、toggle-status(停用/启用)、reset-password(重置密码)、force-offline(强制下线)。data: [{ id, machineCode, firstSeenAt, lastSeenAt }]。4.5 云变量 / 云函数(cloud)
?appId=)。字段:appId, name([A-Za-z0-9_.-]{1,64}), value, mode(0|1), status(ACTIVE|FROZEN)。appId, name, type(HTTP_PROXY|HMAC_SIGN|CHALLENGE), method(GET|POST), targetUrl, headers(JSON 串), secret, timeoutSec(1-30), mode, status。status = FROZEN 即冻结,客户端调用返回 4009,但不删除数据,可随时恢复。05防护机制汇总
| 攻击面 | 防护 |
|---|---|
| 抓包分析协议/参数 | 全报文 AES-256-GCM 加密,无 secret 不可解密;可选响应加密(X-Proto-Ver: 2) |
| 篡改请求 | GCM 认证 + HMAC 双重完整性校验 |
| 重放攻击 | timestamp ±300s 窗口 + nonce 5 分钟唯一 |
| 伪造会话 token | token = 随机 256bit + HMAC(secret) 签名,服务端先本地验签再查库 |
| 口令爆破 | 登录/兑换/管理端均为连续失败 5 次锁定 15 分钟 + IP 令牌桶限流 |
| 多设备滥用 | 机器绑定(machineCode + maxDevices)+ 管理端解绑 |
| 二次打包/改包调用 | 客户端哈希白名单(requireClientHash + allowedClientHashes) |
| 关键逻辑被本地绕过 | 关键判定上云:云变量/HMAC_SIGN/CHALLENGE,密钥不出服务端 |
| SQL 注入 | Prisma 全参数化查询 + class-validator 严格校验 + 枚举白名单 |
| SSRF | HTTP_PROXY 目标地址白名单校验(禁内网/回环/IPv6 私网,禁重定向) |
| 时序攻击 | 签名/密码比较均为常数时间实现 |
| 卡密枚举 | 100bit 熵随机卡密 + 限流 |
06反破解最佳实践
平台提供机制,强度取决于用法。建议组合使用:
- 关键判定上云:不要把「是否放行」的 if 留在本地。例(HMAC_SIGN 双层校验):
- 客户端把机器码+用户名作为 payload 调
HMAC_SIGN云函数,拿到服务端签名sign + timestamp - 客户端把
sign与本地用内置公钥/算法二次校验的材料比对,并校验 timestamp 新鲜度(±5 分钟) - 破解者拿不到云端 secret,无法离线生成合法签名;即使抓包一次,timestamp 过期后也无法重放
- 客户端把机器码+用户名作为 payload 调
- 云资源定期改名:云变量/云函数 name 支持更新。定期(如每月)改名并发布新客户端,让流通的「调用脚本」迅速失效;旧名称可置为 FROZEN 观察调用量
- 机器绑定 + 哈希白名单组合:
machineBind + requireClientHash同时开启,既能限制一号多机,又能识别非官方客户端(改包后哈希变化直接 4008) - SDK 自我保护与加壳:在客户端侧做完整性自检、反调试,并对可执行文件加壳/混淆。第三方加固工具(各平台加壳器、混淆器)由开发者自行选型,本平台不作内置也不背书
- 区分冻结(4009)与网络故障:SDK 收到 4009 应提示「功能被停用/风控」而不是无限重试;网络错误(超时/5xx)才走重试退避。冻结是管理者的主动开关,可随时恢复
07快速接入(三步)
- 建应用:登录管理台(/admin)→ 应用管理 → 新建应用,按需开启机器绑定/哈希白名单/响应加密
- 复制参数:在应用详情复制
baseUrl、appKey、appSecret(appSecret 只显示一次,泄露立即「重置密钥」),并在卡密管理生成测试卡密。管理台「接入指引」页提供对应语言的接入代码模板 - 跑示例:参考
sdk/cpp/example/main.cpp或sdk/python/example.py,用测试卡密跑通 兑换 → 登录 → 心跳 → 登出 全流程,再集成进你的程序
server/src/common/utils/crypto.ts。