DEVELOPER GUIDE
客户端接入文档
Keyflow 的客户端协议是一组小型 JSON HTTP 接口。激活后获得会话令牌,通过心跳维持在线状态;需要完全离线运行时,可申请 Ed25519 签名许可证。
Base URL/api/v1所有请求与响应均为 application/json; charset=utf-8。请求体限制 64 KB。
接入流程
- 创建产品:在控制台设置产品编码(如
desktop-pro)、设备上限、心跳间隔与功能权益。
- 生成卡密:批量生成后通过你的渠道分发给用户。卡密明文只在生成时显示一次。
- 激活:客户端提交卡密与设备指纹,获得
session_token 与 features。
- 心跳:按
heartbeat_after_seconds 定时调用心跳,失败时降级或退出功能。
- 离线(可选):申请签名许可证,客户端用内置公钥离线校验。
认证方式
激活与离线许可证签发不需要令牌。心跳与登出使用激活返回的会话令牌:
Authorization: Bearer <session_token>
会话令牌仅以 SHA-256 摘要存储,服务端无法还原明文。请保存在操作系统安全存储中。
激活授权
POST /api/v1/activations
{
"product_code": "desktop-pro",
"card_key": "A1B2C-3D4E5-F6G7H-8J9K2",
"device": {
"fingerprint": "sha256-of-stable-device-id",
"name": "Design Station",
"os": "windows",
"arch": "amd64",
"client_version": "1.4.0"
}
}
成功响应:
{
"session_token": "9f2c…",
"session_expires_at": "2026-10-24T12:00:00Z",
"server_time": "2026-10-24T11:55:00Z",
"heartbeat_after_seconds": 60,
"remote_config": {
"maintenance": false,
"announcement": "v1.5 已发布"
},
"license": {
"id": "lic_…",
"product_code": "desktop-pro",
"status": "active",
"expires_at": "2026-11-23T11:55:00Z",
"features": ["pro", "export", "api-access"]
}
}
幂等重试Idempotency-Key网络超时后可用同一幂等键重试,服务端会返回完全相同的响应。相同键配合不同请求体会返回 409。
设备指纹建议
- 使用稳定的硬件或安装标识,不要使用会随网络变化的 MAC 地址。
- 对原始标识做 SHA-256 后再上传,客户端不应上传明文硬件信息。
- 指纹变化会消耗一个新的设备名额,因此升级或重装时应尽量保持标识稳定。
设备试用
POST /api/v1/trials,不需要卡密。产品需在后台开启「允许试用」并配置试用天数。
{
"product_code": "desktop-pro",
"device": {
"fingerprint": "sha256-of-stable-device-id",
"name": "Design Station",
"os": "windows",
"arch": "amd64",
"client_version": "1.4.0"
}
}
成功响应与激活一致,并额外返回生成的试用卡密:
{
"session_token": "9f2c…",
"session_expires_at": "2026-10-27T11:55:00Z",
"server_time": "2026-10-24T11:55:00Z",
"heartbeat_after_seconds": 60,
"trial": true,
"card_key": "TRIAL-…",
"remote_config": {},
"license": { "type": "trial", "features": ["pro"], "expires_at": "2026-10-27T11:55:00Z" }
}
- 同一产品的同一设备指纹只能领取一次,重复领取返回
409 TRIAL_ALREADY_CLAIMED。 - 试用授权默认绑定 1 台设备,到期后自动失效,不占用正式卡密的设备名额。
- 产品未开启试用返回
403 TRIAL_DISABLED。
心跳续期
POST /api/v1/sessions/heartbeat(Bearer 令牌)
{
"status": "active",
"session_expires_at": "2026-10-24T12:00:00Z",
"server_time": "2026-10-24T11:59:00Z",
"heartbeat_after_seconds": 60,
"license_expires_at": "2026-11-23T11:55:00Z",
"features": ["pro", "export", "api-access"],
"remote_config": {
"maintenance": false,
"announcement": "v1.5 已发布"
}
}
授权被冻结、吊销、到期或设备被解绑时,心跳会返回 403,客户端应立即锁定功能。网络失败可短暂宽限,但不应无限期离线运行。
远程变量
GET /api/v1/config(Bearer 令牌)可在不续期、不重新激活的情况下拉取最新配置。适合在设置页打开、切换功能模块或长连接空闲时调用。
{
"status": "active",
"server_time": "2026-10-24T11:59:30Z",
"heartbeat_after_seconds": 60,
"remote_config": { "maintenance": false, "max_export_rows": 5000 },
"features": ["pro", "export"],
"license": {
"id": "lic_…",
"type": "duration",
"status": "active",
"expires_at": "2026-11-23T11:55:00Z"
}
}
远程变量在产品上配置默认值,单张授权可覆盖。合并规则:以产品默认值为底,授权覆盖层逐键覆盖;值为 null 表示删除该继承键。变量同时随激活、心跳、API 校验和本接口下发。
安全建议远程变量用于功能开关、公告、限流阈值等非敏感配置。不要下发密钥、数据库口令等机密,也不要仅依赖客户端开关做授权判断。
主动登出
POST /api/v1/sessions/logout(Bearer 令牌),成功返回 204 No Content。
离线许可证
POST /api/v1/offline-licenses(Bearer 令牌)。产品必须启用离线签名。
{
"payload": "eyJzY2hlbWFfdmVyc2lvbiI6…",
"signature": "5m3n…",
"kid": "ed25519_ab12cd34ef56"
}
payload 是 Base64URL 编码的 JSON 声明,signature 是对原始 payload 字节的 Ed25519 签名。声明结构:
{
"schema_version": "network-auth.license.v1",
"grant_id": "grt_…",
"license_id": "lic_…",
"product_code": "desktop-pro",
"product_name": "Desktop Pro",
"device_hash": "sha256…",
"features": ["pro", "export"],
"issued_at": "2026-10-24T11:55:00Z",
"not_before": "2026-10-24T11:55:00Z",
"expires_at": "2026-11-23T11:55:00Z",
"revocation_epoch": 3
}
客户端校验步骤
- Base64URL 解码
payload 与 signature。 - 用内置公钥验证签名,拒绝任何验证失败的载荷。
- 校验
device_hash 与当前设备一致。 - 校验
not_before / expires_at 与系统时间。 - 定期获取吊销列表,若许可证被吊销或
revocation_epoch 落后则拒绝。
当前签名密钥ed25519_c38e0dbb76bd通过 GET /api/v1/signing-keys 获取 Base64URL 编码的原始 Ed25519 公钥。
签名公钥
GET /api/v1/signing-keys
{
"keys": [
{
"kid": "ed25519_ab12cd34ef56",
"algorithm": "Ed25519",
"public_key": "Base64URL-encoded-32-byte-key"
}
]
}
将公钥编译进客户端。签名私钥只保存在服务端,不会通过网络传输。
吊销列表
GET /api/v1/revocations?product_code=desktop-pro
{
"product_code": "desktop-pro",
"revocation_epoch": 4,
"revoked": [
{ "license_id": "lic_…", "revoked_at": "2026-10-20T08:30:00Z" }
],
"server_time": "2026-10-24T11:55:00Z"
}
吊销授权会同时递增产品的 revocation_epoch。离线客户端可缓存该列表,并在联网时刷新。
版本接口
获取最新发布版本:
GET /api/v1/releases/latest?product_code=desktop-pro&channel=stable
列出全部已发布版本:
GET /api/v1/releases?product_code=desktop-pro
响应包含 version、download_url、checksum、mandatory、notes 与 published_at。下载后请自行校验 sha256。
Webhook
控制台创建 Webhook 后可订阅授权事件。每次投递包含以下请求头:
X-Keyflow-Event: license.activate
X-Keyflow-Delivery: whd_…
X-Keyflow-Signature: sha256=<hex hmac>
Content-Type: application/json
签名计算方式:HMAC-SHA256(secret, raw_request_body)。请使用常量时间比较,并校验时间戳/事件 ID 防止重放。
{
"id": "whd_…",
"event": "license.activate",
"created_at": "2026-10-24T11:55:00Z",
"data": {
"license_id": "lic_…",
"device_id": "dev_…",
"session_id": "ses_…",
"product_code": "desktop-pro"
}
}
支持事件:license.activate、license.freeze、license.unfreeze、license.revoke、device.unbind、session.revoke、release.published、customer.register、customer.redeem,或 * 订阅全部。
错误码
| HTTP | code | 含义 | 建议处理 |
| 400 | INVALID_REQUEST | 字段缺失或格式错误 | 检查请求体,不要自动重试 |
| 401 | SESSION_EXPIRED | 会话不存在或已过期 | 重新激活 |
| 401 | SESSION_REVOKED | 会话被管理员或解绑操作撤销 | 提示用户重新登录/激活 |
| 403 | LICENSE_UNAVAILABLE / REVOKED / FROZEN / EXPIRED | 授权不可用 | 停止业务功能并提示原因 |
| 403 | DEVICE_LIMIT_REACHED | 设备数达到上限 | 提示用户解绑旧设备 |
| 403 | DEVICE_BLOCKED | 设备被禁用 | 联系管理员 |
| 403 | OFFLINE_DISABLED | 产品未启用离线许可证 | 使用在线模式 |
| 403 | TRIAL_DISABLED | 产品未开启试用 | 改用卡密激活 |
| 409 | TRIAL_ALREADY_CLAIMED | 该设备已领取过试用 | 提示购买正式授权 |
| 409 | IDEMPOTENCY_CONFLICT | 幂等键复用于不同请求 | 更换幂等键 |
| 429 | RATE_LIMITED | 请求过于频繁 | 按 Retry-After 退避 |
SDK 快速开始
本仓库在 sdk/ 下提供 Go、TypeScript、C#/.NET 与 C++ 四套实现,接口、错误码和离线验签行为保持一致。完整安装说明见各目录的 README。
Go
client := keyflow.New(keyflow.Options{
BaseURL: "https://license.example.com",
ProductCode: "desktop-pro",
})
session, err := client.Activate(ctx, keyflow.ActivationInput{
CardKey: card,
Fingerprint: fingerprint,
DeviceName: hostname,
})
if err != nil {
log.Fatal(err)
}
if session.HasFeature("export") {
enableExport()
}
TypeScript
import { KeyflowClient } from '@keyflow/client';
const client = new KeyflowClient({
baseUrl: 'https://license.example.com',
productCode: 'desktop-pro',
});
const session = await client.activate({
cardKey,
fingerprint,
deviceName: 'Design Station',
});
if (session.license.features.includes('export')) enableExport();
C# / .NET
var client = new KeyflowClient(new KeyflowOptions {
BaseUrl = "https://license.example.com",
ProductCode = "desktop-pro",
});
var session = await client.ActivateAsync(new ActivationRequest {
CardKey = card,
Fingerprint = fingerprint,
DeviceName = Environment.MachineName,
});
C++
keyflow::Client client("https://license.example.com", "desktop-pro");
keyflow::ActivationInput input;
input.card_key = card;
input.fingerprint = fingerprint;
input.device_name = "Design Station";
auto session = client.activate(input);
if (session.has_feature("export")) enable_export();
试用与远程变量
各 SDK 均提供 StartTrial(无需卡密)与 Config(刷新远程变量)。会话对象会缓存最近一次下发的远程变量,心跳成功后自动更新。
// Go
trial, err := client.StartTrial(ctx, keyflow.ActivationInput{Fingerprint: fingerprint})
snapshot, err := trial.Session.RefreshConfig(ctx)
fmt.Println(trial.CardKey, snapshot.RemoteConfig["announcement"])
// TypeScript
const trial = await client.startTrial({ fingerprint });
const snapshot = await trial.session.refreshConfig();
console.log(trial.cardKey, trial.session.remoteConfig);
// C# / .NET
var trial = await client.StartTrialAsync(new TrialRequest { Fingerprint = fingerprint });
var snapshot = await trial.Session.RefreshConfigAsync();
Console.WriteLine(snapshot.RemoteConfig["announcement"]);
离线验签
从 /api/v1/signing-keys 获取公钥后按 kid 缓存,验证签名、时间窗口、device_hash 与吊销 epoch。生产环境可在构建时固定公钥,避免运行期替换。各 SDK 均提供 VerifyOfflineLicense 等价方法。
浏览器与 CORS
授权 API 默认不开放跨域。浏览器应用需在服务端设置 CORS_ALLOWED_ORIGINS(逗号分隔的完整 Origin),预检请求会自动放行 Authorization、Content-Type 与 Idempotency-Key。Electron 主进程、Node.js 与服务端接入无需 CORS。不要在前端页面中内置卡密。