快速开始
整体只有三步:
- 联系我们申请一组接口密钥,拿到
key_id与secret(secret 只下发一次,请妥善保存)。 - 客户端采集设备指纹
device_id(HWID 的 SHA-256),连同激活码调用/api/v1/activate。 - 把返回的
license_file保存到本地,以后启动时先本地验签,再按heartbeat_interval_hours做在线心跳。
密钥属于敏感凭据:请只在客户端首次激活时使用,不要硬编码进可被反编译的位置;
如需轮换或停用,请随时联系我们。
接口鉴权(HMAC 签名)
每个请求需要携带 4 个请求头:
| 请求头 | 说明 |
|---|---|
X-Yomur-Key | API Key 的 key_id,例如 yk_1a2b3c4d5e6f |
X-Yomur-Timestamp | 毫秒时间戳;与服务端相差需在 ±5 分钟内 |
X-Yomur-Nonce | 随机串,10 分钟内不可重复(防重放) |
X-Yomur-Signature | HMAC-SHA256 十六进制签名,见下 |
签名算法:
bodyHash = SHA256_HEX(rawBody) // 原始请求体字节的 sha256(GET 请求传空串)
base = `${key_id}.${timestamp}.${nonce}.${bodyHash}`
signature = HMAC_SHA256_HEX(key = SHA256_HEX(secret), message = base)
服务端只保存 SHA256(secret),因此客户端计算签名时也用 SHA256(secret) 作为 HMAC 密钥 —— 这样平台无需存储明文密钥。
// Node.js 参考实现
const crypto = require('node:crypto');
function sign({ keyId, secret, body }) {
const timestamp = Date.now().toString();
const nonce = crypto.randomBytes(8).toString('hex');
const raw = body ? JSON.stringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(raw).digest('hex');
const base = `${keyId}.${timestamp}.${nonce}.${bodyHash}`;
const hmacKey = crypto.createHash('sha256').update(secret).digest('hex');
const signature = crypto.createHmac('sha256', hmacKey).update(base).digest('hex');
return { timestamp, nonce, signature, raw };
}
POST /api/v1/activate — 激活设备
把当前设备绑定到激活码。首次激活占用一个设备额度;同一设备重复激活不会重复占用。
| 字段 | 类型 | 说明 |
|---|---|---|
license_key | string | 必填。用户输入的激活码 |
device_id | string | 必填。设备指纹(≥8 位,建议 32 位十六进制) |
device_name | string | 选填。设备名,用于后台展示 |
os | string | 选填。操作系统版本 |
app_version | string | 选填。客户端版本号 |
product_slug | string | 选填。用于校验激活码是否属于本产品 |
POST /api/v1/activate
Content-Type: application/json
X-Yomur-Key: yk_xxxxxxxxxxxx
X-Yomur-Timestamp: 1735689600000
X-Yomur-Nonce: 9f2c1a7b3d4e5f60
X-Yomur-Signature: <hmac>
{ "license_key": "YOMUR-3F9KA-8M2QV-XT7YB-5HNDP-9",
"device_id": "A1B2C3D4E5F60718293A4B5C6D7E8F90",
"device_name": "办公室台式机",
"os": "Windows 11 23H2",
"app_version": "1.0.0",
"product_slug": "yomur-keygen-demo" }
成功响应(HTTP 200):
{
"ok": true,
"code": "ok",
"message": "激活成功",
"already_bound": false,
"license": {
"license_key": "YOMUR-3F9KA-8M2QV-XT7YB-5HNDP-9",
"status": "active",
"duration_days": 0,
"max_devices": 2,
"device_count": 1,
"expires_at": null,
"devices": [ { "device_id": "A1B2...", "device_name": "办公室台式机", "last_seen_at": "..." } ]
},
"license_file": "YOMUR-LIC-v1.eyJ2IjoxLCJrZXki...<签名>",
"activation_token": "eyJsaWMiOjEsImRldiI6...",
"offline_grace_days": 7,
"heartbeat_interval_hours": 72,
"server_time": "2025-01-01T00:00:00.000Z"
}
激活失败时返回 HTTP 400,body 中包含 code 与 message(见「错误码」)。
POST /api/v1/verify — 心跳 / 在线校验
建议每 heartbeat_interval_hours 小时调用一次,用于感知吊销、过期与解绑。
{ "license_key": "YOMUR-...", "device_id": "A1B2...", "app_version": "1.0.0" }
// 或使用激活时返回的令牌(无需再传 device_id)
{ "activation_token": "eyJsaWMiOjEsImRldiI6...", "app_version": "1.0.0" }
返回 { ok, code, status, license{...}, license_file };ok=false 且 code 为 revoked / expired / suspended / device_unbound 时,客户端应限制功能。
POST /api/v1/deactivate — 解绑当前设备
解绑后释放一个设备额度。是否允许自助解绑、以及解绑冷却时间,由产品设置决定(后台可配)。
{ "license_key": "YOMUR-...", "device_id": "A1B2..." }
// 或 { "activation_token": "eyJsaWMiOjEsImRldiI6..." }
错误码
| code | HTTP | 含义与建议处理 |
|---|---|---|
ok | 200 | 成功 |
bad_format | 400 | 激活码格式/校验位错误 → 提示用户核对 |
not_found | 400 | 激活码不存在 → 提示检查 |
device_limit | 400 | 设备数达上限 → 引导到「激活与设备」页解绑 |
expired | 400 | 授权过期 → 引导续费 |
revoked | 400 | 已吊销(如退款)→ 停止使用 |
suspended | 400 | 被暂停 → 联系客服 |
product_mismatch | 400 | 激活码不属于当前产品 |
device_unbound | 400 | 当前设备已被解绑 → 重新激活 |
device_not_activated | 400 | 该设备尚未激活 |
bad_signature / timestamp_expired / nonce_replayed | 401 | 签名或时间校验失败 → 检查密钥与系统时钟 |
rate_limited | 429 | 请求过于频繁 → 退避重试 |
离线验签(Ed25519)
许可文件格式:YOMUR-LIC-v1.<payloadBase64Url>.<signatureBase64Url>
验签步骤:
- 用
/api/public-key拿到 Ed25519 公钥(SPKI PEM),内置进客户端; - 对 payload 的原始 JSON 字符串(UTF-8) 做 Ed25519 验签;
- 验签通过后,再校验 payload 中的
expires_at、max_devices与本地缓存的在线校验时间; - 超过
grace_days未成功联网校验时,可降级为受限模式。
const crypto = require('node:crypto');
const [tag, dataB64, sigB64] = licenseFile.trim().split('.');
const payloadJson = Buffer.from(dataB64, 'base64url').toString('utf8');
const ok = crypto.verify(null, Buffer.from(payloadJson, 'utf8'),
crypto.createPublicKey(publicKeyPem),
Buffer.from(sigB64, 'base64url'));
网站端接口(下单 / 查询 / 下载)
| 接口 | 说明 |
|---|---|
GET /api/site | 站点公开配置 + 产品列表 |
GET /api/products/:slug | 产品详情(含套餐与版本) |
POST /api/orders | 创建订单并返回支付链接 |
GET /api/orders/:no?email= | 查询订单(需邮箱匹配) |
POST /api/orders/:no/pay | 切换支付方式并重新生成支付单 |
GET /api/download/:token | 凭签名令牌下载安装包 |
GET /api/license/lookup | 激活码 + 邮箱查询授权与设备 |
POST /api/license/unbind | 自助解绑设备 |
GET /api/license/file | 下载 .lic 离线许可文件 |
需要 Node / 桌面端的完整参考实现(签名、激活、心跳、离线验签与设备指纹采集),请通过 客服邮箱 联系我们获取。