桌面客户端激活码接入文档
推荐按 License v2 接入:后台准备 projectKey 和激活码,客户端生成 machineId 与 Ed25519 设备密钥,用户输入激活码时 enroll,随后用短期 token 和设备签名调用 status / consume。
先看这 3 件事
新客户端不用再把 API Secret 放进本地程序。先准备项目和激活码,再让客户端保存稳定 machineId 与设备私钥,最后按 enroll、challenge、renew、status、consume 五个 v2 接口接入。 所有激活码客户端均使用 License v2 的设备签名接口。
Python 客户端不再内置 API Secret;本机生成 Ed25519 设备密钥,后台项目会提供离线验签公钥。
enroll 绑定设备、公钥和指纹,challenge / renew 续租短期 token,status / consume 每次请求都签名。
客户端需要准备什么
Python 桌面程序通常内置 Base URL 和 projectKey,激活码由用户输入。machineId 和 Ed25519 私钥首次运行生成后保存在本机,以后一直复用。
License v2 签名怎么用
v2 不把 API Secret 放进客户端。客户端用 Ed25519 设备私钥签名请求,服务端用 enroll 时登记的设备公钥验证。签名会绑定路径、session、时间戳、nonce、请求体 hash 和 token hash。
import base64
import hashlib
import json
import os
import time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = Ed25519PrivateKey.generate()
license_token = "eyJ..."
session_id = "ls_xxx"
path = "/api/license/v2/status"
body = json.dumps({}, separators=(",", ":"), ensure_ascii=False)
timestamp = str(int(time.time()))
nonce = "py_" + base64.urlsafe_b64encode(os.urandom(18)).rstrip(b"=").decode("ascii")
body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
token_hash = hashlib.sha256(license_token.encode("utf-8")).hexdigest()
canonical = "\n".join([
"LICENSE-V2-PROOF",
"POST",
path,
session_id,
timestamp,
nonce,
body_hash,
token_hash,
])
signature = base64.urlsafe_b64encode(
private_key.sign(canonical.encode("utf-8")),
).rstrip(b"=").decode("ascii")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {license_token}",
"X-License-Session-Id": session_id,
"X-License-Timestamp": timestamp,
"X-License-Nonce": nonce,
"X-License-Signature": signature,
}完整接入流程
下面是桌面客户端最常见的接入顺序。照这个流程做,用户换设备、网络重试、次数扣减和后台排查都能对应起来。
后台创建项目和激活码
在后台项目管理里创建项目,生成 TIME 或 COUNT 激活码,并复制该项目的 License v2 离线公钥。
客户端固定 projectKey 和项目级 offline public key;不再使用 API Secret。
客户端生成设备密钥和 machineId
Python 程序首次运行时生成稳定 machineId、设备指纹 hash 和 Ed25519 私钥。私钥保存在系统钥匙串、DPAPI、Keychain 或安全文件存储里。
服务端保存设备公钥和 fingerprintHash;后续请求必须由本机私钥签名并携带匹配指纹。
用户输入激活码后调用 enroll
把 code、machineId、projectKey、fingerprintHash、devicePublicKey 和 deviceSignature 发到 /api/license/v2/enroll。
服务端验证设备私钥持有证明,绑定设备并下发短期 licenseToken。
续租短期 token
token 快过期时先调用 /api/license/v2/challenge,再用设备私钥签名 challenge 调用 /api/license/v2/renew。
短期 token 和设备签名同时存在,复制 token 不能长期绕过授权。
启动检查和扣次都走签名请求
程序启动调用 /api/license/v2/status,COUNT 次数卡真实使用成功后调用 /api/license/v2/consume。
每次请求都签名方法、路径、session、时间戳、nonce、请求体 hash 和 token hash,服务端防重放。
检测设备指纹漂移
status、consume、challenge 和 renew 会复核已绑定 fingerprintHash;缺失或变化会拒绝请求并记录安全事件。
复制 session、token 或设备私钥到另一台机器时,还会触发 HostID / fingerprint 漂移防护。
在后台日志里排查问题
遇到客户反馈时,用激活码、machineId、sessionId 或安全事件在后台查询。
可以确认设备是否被吊销、客户端版本是否被封禁、是否发生签名失败、nonce 重放或指纹漂移。
可选启用离线签名授权快照
新建项目会自动生成项目级 Ed25519 离线签名密钥;老项目启动时会自动补齐。环境变量级密钥仅作为兼容兜底。
客户端用项目级公钥在短暂断网时验证授权快照,适合弱网宽限和只读状态校验。
客户端调用明细
下面按真实客户端生命周期展开:哪些动作在本机完成、哪些接口要调用、服务端会校验什么、成功后客户端要保存什么。
后台:/api/admin/projects + /api/admin/codes/generate
本地动作,无需请求服务端
POST /api/license/v2/enroll
POST /api/license/v2/status
POST /api/license/v2/consume
POST /api/license/v2/challenge -> POST /api/license/v2/renew
响应字段:offlineLicense / offlineLicenseExpiresAt / offlineLicensePublicKey
公共请求参数
License v2 接口共用以下字段。projectKey 用于项目隔离,code 是激活码正文,machineId 表示设备,devicePublicKey / deviceSignature 用于注册设备,requestId 用于 consume 幂等扣次。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| projectKey / project_key | string | 否 | 项目标识。建议每个桌面软件或产品使用独立 projectKey;不传时走 default 项目。 |
| code | string | 是 | 后台生成并发放给用户的激活码正文。 |
| machineId / machine_id | string | 是 | 设备唯一标识。必须稳定保存,不能每次启动重新生成。 |
| fingerprintHash / fingerprint_hash | string | v2 推荐 | 客户端本地计算的设备指纹 hash。enroll 后服务端绑定该值,status、consume、challenge、renew 缺失或漂移都会被拒绝并记录安全事件。 |
| requestId / request_id | string | 仅 consume 推荐 | 每次真实业务动作的唯一 ID。网络重试时复用同一个值,不会重复扣次。 |
| devicePublicKey | string | v2 enroll 是 | base64url 编码的 32 字节 Ed25519 公钥。服务端保存公钥,客户端保管私钥。 |
| deviceSignature | string | v2 enroll 是 | 客户端用设备私钥签名 enroll canonical message,证明持有对应私钥。 |
| licenseToken | string | v2 challenge / signed request 是 | 服务端下发的短期授权 token。正式请求建议放在 Authorization: Bearer 头里。 |
| sessionId / challengeId / nonce / signature | string | v2 renew 是 | challenge / renew 续租流程字段。signature 是设备私钥对服务端 signInput 的 Ed25519 签名。 |
响应数据结构
响应同时返回 camelCase 和 snake_case 字段,便于新旧客户端共存。前端展示授权状态时优先使用 licenseMode、expiresAt、remainingCount、valid 和 message。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| success | boolean | 总是返回 | 业务是否通过。客户端一定要判断这个字段,不要只看 HTTP 状态码。 |
| message | string | 总是返回 | 失败原因或成功提示,可直接用于日志,也可整理后展示给用户。 |
| licenseMode / license_mode | TIME | COUNT | null | 按场景返回 | 授权模型;TIME 表示有效期授权,COUNT 表示按次数消费。 |
| expiresAt / expires_at | string | null | 时间型常用 | TIME 模式的过期时间;COUNT 模式通常为空。 |
| remainingCount / remaining_count | number | null | 次数型常用 | COUNT 模式的剩余次数;TIME 模式通常为空。 |
| isActivated / is_activated | boolean | null | 按场景返回 | 当前激活码是否已经绑定到设备。 |
| valid | boolean | null | 按场景返回 | 当前授权是否仍可继续使用。启动检查时重点看 success 和 valid。 |
| idempotent | boolean | null | consume 常用 | consume 是否命中 requestId 幂等重放;true 表示没有再次扣次。 |
| licenseToken | string | v2 enroll / renew 返回 | License v2 短期 token。客户端保存后,status / consume 请求必须同时带 token 和设备签名。 |
| sessionId / deviceId / tokenExpiresAt | string | number | v2 常用 | 服务端会话、设备记录和 token 过期时间,用于续租和后台排查。 |
| signInput | string | v2 challenge 返回 | 服务端生成的一次性 challenge canonical message,客户端直接用设备私钥签名后提交 renew。 |
| offlineLicense / offlineLicenseExpiresAt / offlineLicensePublicKey | string | v2 可选返回 | 项目级私钥签发的短期授权快照。正式客户端应固定后台项目里的 offline public key,并校验 projectKey、machineId、fingerprintHash、有效期和授权状态。 |
新版 License v2(推荐)
给新的 Python 桌面程序和需要高强度防逆向的客户端使用。客户端不内置 API Secret,所有正式请求都绑定短期 token 和 Ed25519 设备签名。
License v2:设备注册和激活
用户第一次输入激活码时调用。客户端提交设备公钥和私钥签名,服务端绑定设备并下发短期 token。
新 Python 客户端、桌面程序、需要防逆向强化的正式接入都优先使用这个入口。
请求说明
{
"projectKey": "browser-plugin",
"code": "A1B2C3D4E5F6G7H8",
"machineId": "machine-001",
"appVersion": "1.0.0",
"fingerprintHash": "sha256-of-device-fingerprint",
"devicePublicKey": "base64url-ed25519-public-key",
"deviceSignature": "base64url-ed25519-signature"
}{
"success": true,
"message": "License v2 设备注册成功",
"tokenType": "LicenseV2",
"licenseToken": "eyJ...",
"sessionId": "ls_xxx",
"expiresAt": "2026-04-27T13:30:00.000Z",
"deviceId": 12,
"licenseMode": "COUNT",
"remainingCount": 2,
"offlineLicense": "amlic2.payload.signature",
"offlineLicenseExpiresAt": "2026-04-28T13:30:00.000Z",
"offlineLicensePublicKey": "base64url-ed25519-public-key"
}License v2:创建续租 challenge
token 快过期时调用,服务端返回一次性 challenge 和 signInput。
客户端定时续租或启动后发现 token 即将过期时调用。
请求说明
{
"sessionId": "ls_xxx",
"licenseToken": "eyJ...",
"fingerprintHash": "sha256-of-device-fingerprint"
}{
"success": true,
"message": "License v2 challenge 创建成功",
"sessionId": "ls_xxx",
"challengeId": "lc_xxx",
"nonce": "nonce_xxx",
"expiresAt": "2026-04-27T12:35:00.000Z",
"signInput": "LICENSE-V2-CHALLENGE\nPOST\n/api/license/v2/renew\n..."
}License v2:续租短期 token
客户端用设备私钥签名 challenge 后调用,服务端验证通过后刷新 session 和 token。
token 过期前续租;推荐在客户端本地缓存 expiresAt,提前几分钟续租。
请求说明
{
"sessionId": "ls_xxx",
"challengeId": "lc_xxx",
"nonce": "nonce_xxx",
"signature": "base64url-ed25519-signature",
"fingerprintHash": "sha256-of-device-fingerprint"
}{
"success": true,
"message": "License v2 session 续租成功",
"tokenType": "LicenseV2",
"licenseToken": "eyJ...",
"sessionId": "ls_xxx",
"expiresAt": "2026-04-27T14:30:00.000Z",
"deviceId": 12,
"licenseMode": "COUNT",
"remainingCount": 2,
"offlineLicense": "amlic2.payload.signature",
"offlineLicenseExpiresAt": "2026-04-28T14:30:00.000Z"
}License v2:签名查询授权状态
程序启动或刷新授权页时调用。请求必须带短期 token 和设备私钥签名。
启动检查、授权信息展示、用户点击刷新授权时调用。
请求说明
POST /api/license/v2/status
Authorization: Bearer eyJ...
X-License-Session-Id: ls_xxx
X-License-Timestamp: 1777280000
X-License-Nonce: py_xxx
X-License-Signature: base64url-ed25519-signature
{
"fingerprintHash": "sha256-of-device-fingerprint"
}{
"success": true,
"message": "获取激活码状态成功",
"licenseMode": "COUNT",
"remainingCount": 2,
"isActivated": true,
"valid": true,
"sessionId": "ls_xxx",
"deviceId": 12,
"tokenExpiresAt": "2026-04-27T14:30:00.000Z",
"offlineLicense": "amlic2.payload.signature",
"offlineLicenseExpiresAt": "2026-04-28T14:30:00.000Z"
}License v2:签名扣次 / 校验权益
COUNT 授权每次真实使用成功后调用一次;TIME 授权调用它只做有效性校验。
导出、生成、分析、识别、下载等付费功能真正完成后调用。
请求说明
POST /api/license/v2/consume
Authorization: Bearer eyJ...
X-License-Session-Id: ls_xxx
X-License-Timestamp: 1777280000
X-License-Nonce: py_xxx
X-License-Signature: base64url-ed25519-signature
{
"requestId": "req-001",
"fingerprintHash": "sha256-of-device-fingerprint"
}{
"success": true,
"message": "激活码验证成功",
"licenseMode": "COUNT",
"remainingCount": 1,
"isActivated": true,
"valid": true,
"idempotent": false,
"sessionId": "ls_xxx",
"deviceId": 12,
"tokenExpiresAt": "2026-04-27T14:30:00.000Z",
"offlineLicense": "amlic2.payload.signature",
"offlineLicenseExpiresAt": "2026-04-28T14:30:00.000Z"
}可直接参考的代码
Python 示例是桌面客户端最小可用接入方式,包含设备密钥、machineId 持久化、token 续租和签名请求。cURL 只用于看请求结构。
适合 PyQt、Tkinter、Flet、命令行工具等 Python 客户端。示例不保存 API Secret,而是本机生成 Ed25519 设备私钥。
# 安装示例依赖
python -m pip install cryptography keyring
# 项目里已提供完整示例:
# examples/python/license_v2_client.py
python examples/python/license_v2_client.py demo \
--base-url https://mi.gxslgg.cn \
--project-key browser-plugin \
--code A1B2C3D4E5F6G7H8 \
--machine-id machine-001 \
--app-version 1.0.0 \
--fingerprint-hash sha256-of-device-fingerprint
# 真实流程:
# 1. enroll: 生成 Ed25519 设备密钥,提交公钥、fingerprintHash 和私钥签名
# 2. challenge + renew: 用设备私钥续租短期 licenseToken,并复核 fingerprintHash
# 3. status / consume: 每次请求都签名 timestamp、nonce、bodyHash、tokenHash 和请求体指纹
# 4. offline-status: 用后台项目里的离线公钥验证本地 offlineLicense 快照和 fingerprintHash适合临时查看请求体结构。v2 的 signature 必须由客户端 Ed25519 私钥生成,正式调用请用 Python 示例或你的客户端代码签名。
# enroll: 用户输入激活码后调用
curl -X POST "https://mi.gxslgg.cn/api/license/v2/enroll" \
-H "Content-Type: application/json" \
-d '{
"projectKey": "browser-plugin",
"code": "A1B2C3D4E5F6G7H8",
"machineId": "machine-001",
"appVersion": "1.0.0",
"fingerprintHash": "sha256-of-device-fingerprint",
"devicePublicKey": "base64url-ed25519-public-key",
"deviceSignature": "base64url-ed25519-signature"
}'
# challenge: 用 sessionId 和 token 换一次性 signInput
curl -X POST "https://mi.gxslgg.cn/api/license/v2/challenge" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ..." \
-d '{
"sessionId": "ls_xxx",
"fingerprintHash": "sha256-of-device-fingerprint"
}'
# status / consume: 必须带 token 和设备签名头
curl -X POST "https://mi.gxslgg.cn/api/license/v2/status" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ..." \
-H "X-License-Session-Id: ls_xxx" \
-H "X-License-Timestamp: 1777280000" \
-H "X-License-Nonce: py_xxx" \
-H "X-License-Signature: base64url-ed25519-signature" \
-d '{"fingerprintHash":"sha256-of-device-fingerprint"}'
# 响应可选包含 offlineLicense,客户端用项目级 Ed25519 公钥验签后可做短期断网宽限。后台需要做什么
客户端不要调用后台接口。后台只负责创建项目、生成激活码、查看 v2 设备和 session、吊销异常设备、封禁客户端版本和查看安全事件。
项目与发码
用于准备接入环境:先创建 projectKey,再为对应项目生成 TIME 或 COUNT 激活码。
创建项目,为不同产品、插件或客户生成独立 projectKey。
维护项目名称、描述、启停状态和项目级策略。
为指定项目批量生成 TIME 或 COUNT 激活码。
查看已发放激活码,反查绑定设备、剩余次数和有效期。
日志与统计
用于排查客户反馈、核对扣次结果和导出运营数据。
按 projectKey、关键词和时间范围查询消费日志。
按当前筛选条件导出消费日志 CSV,便于对账和留档。
查看 1-90 天消费趋势,支持按日、周、月聚合。
查看全局和项目级授权统计。
License v2 安全管理
用于管理设备密钥、短期会话、安全事件和客户端版本封禁。
分页查看 v2 设备,支持按 projectKey、状态、版本和关键词筛选。
吊销异常设备,并同步吊销该设备下的活跃 session。
分页查看 v2 短期会话,排查 token 续租和过期问题。
按项目封禁指定客户端版本,用于阻断已破解或存在高风险的版本。
查看签名失败、nonce 重放、版本封禁、设备吊销等安全事件。