许可证激活
应用如何调用 activate、verify、deactivate 完成许可证激活、启动校验和换机,如何选择设备标识,以及用 Ed25519 签名的 token 离线验证许可证。
你的应用需要调用三个许可证接口:用户输入密钥时激活一次,应用启动时校验,用户换电脑时停用。三个接口都不需要 API 密钥,许可证密钥本身就是凭证,所以应用里没有需要藏起来的机密。
三个接口都接收 JSON,路径都在你的 Keygate 地址下的 /api/v1/license。具体框架的完整代码见 Electron、Tauri、.NET、Python 和 Swift 的接入指南。
工作原理
Keygate 为什么用随机密钥加签名 token,而不是让应用自己校验密钥,见 How license keys work(英文)。
- 用户输入许可证密钥后,应用带上密钥和设备标识调用
activate。Keygate 记录这次激活,并返回一个签名 token。 - 应用保存密钥和 token,每次启动时在本地检查 token。只要 token 还在联网校验间隔内,应用就直接运行,完全不用联网。
- token 到期后,应用调用
verify,拿到新的 token,以及当前的套餐和功能。如果这期间许可证被取消或吊销,返回结果里会说明。 - 用户换电脑时,应用调用
deactivate,空出来的名额就可以给新机器用。
选择设备标识
identifier 告诉 Keygate 这次激活属于哪台设备。同一台机器的值必须保持不变,否则每次重启都会被当成新设备,白白占掉一个名额。比较好的来源是操作系统自带的机器 ID:macOS 上的 IOPlatformUUID,Windows 注册表里的 MachineGuid,Linux 上的 /etc/machine-id。发送前先做一次哈希是个好习惯,因为你只需要这个值稳定,不需要它可读。
如果你按人授权而不是按机器授权,就把用户的邮箱作为 identifier 发送。激活只适用于桌面和混合类型的产品,SaaS 产品按席位计算人数。
激活
curl -X POST https://licenses.example.com/api/v1/license/activate \
-H "Content-Type: application/json" \
-d '{
"license_key": "KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789",
"identifier": "9f2c41e07b6a",
"label": "Office iMac"
}'| 字段 | 必填 | 说明 |
|---|---|---|
license_key | 是 | 客户输入的密钥。 |
identifier | 是 | 设备的稳定 ID,或者用户的邮箱。 |
identifier_type | 否 | device(默认)或 user,作为标签随激活记录保存。 |
label | 否 | 客户在门户里能认出来的名字,比如电脑名称。 |
成功时返回:
{
"success": true,
"data": {
"status": "activated",
"license_id": "4f0c6c1e-2a7b-4d4e-9a55-0d3b7e1f2c11",
"token": "eyJsaWQiOiI0ZjBj...In0.Xk3m9QJ..."
}
}已激活的设备再次调用 activate 会返回 already_activated,不会多占名额,所以拿不准的时候随时调用都没问题。
启动时校验
curl -X POST https://licenses.example.com/api/v1/license/verify \
-H "Content-Type: application/json" \
-d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","identifier":"9f2c41e07b6a"}'{
"success": true,
"data": {
"status": "active",
"plan_id": "c2b7...",
"plan_name": "Pro",
"valid_until": "2027-10-04T00:00:00Z",
"features": { "export_pdf": true, "projects": "50" },
"grace_days": 7,
"token": "eyJsaWQiOiI0ZjBj...In0.Q8vR2..."
}
}永不过期的许可证不会返回 valid_until。带更新期的永久许可证还会返回 updates_until,也就是允许安装的版本的最晚发布日期。
离线使用
activate 和 verify 返回的结果里都带有一个用 Ed25519 签名的 token。应用不联网也能校验它,所以在飞机上或防火墙后面也能正常启动。
获取公钥
验证用的公钥只需获取一次,打包进应用即可。只有你在服务端更换了 LICENSE_SIGNING_KEY,它才会变。
curl https://licenses.example.com/api/v1/license/pubkey
{"success":true,"data":{"algorithm":"ed25519","format":"hex","public_key":"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"}}校验 token
token 由两段 base64url 字符串用点连接而成:前一段是 payload,后一段是它的签名。签名针对的是 token 里原样的 payload,也就是解码之前的内容。Node.js 示例:
import { createPublicKey, verify } from "node:crypto";
const PUBLIC_KEY_HEX = "3b6a27bc..."; // from /api/v1/license/pubkey
// Wrap the raw 32 byte key in the DER header Node expects for Ed25519.
const publicKey = createPublicKey({
key: Buffer.concat([
Buffer.from("302a300506032b6570032100", "hex"),
Buffer.from(PUBLIC_KEY_HEX, "hex"),
]),
format: "der",
type: "spki",
});
export function readToken(token, identifier) {
const [payload, signature] = token.split(".");
const valid = verify(null, Buffer.from(payload), publicKey, Buffer.from(signature, "base64url"));
if (!valid) return null;
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
if (claims.did !== identifier) return null; // issued to another device
return claims;
}Go 的写法:
func readToken(token, identifier string, publicKey ed25519.PublicKey) (map[string]any, bool) {
payload, signature, ok := strings.Cut(token, ".")
if !ok {
return nil, false
}
sig, err := base64.RawURLEncoding.DecodeString(signature)
if err != nil || !ed25519.Verify(publicKey, []byte(payload), sig) {
return nil, false
}
raw, err := base64.RawURLEncoding.DecodeString(payload)
if err != nil {
return nil, false
}
var claims map[string]any
if json.Unmarshal(raw, &claims) != nil || claims["did"] != identifier {
return nil, false
}
return claims, true
}token 里有什么
所有时间都是 Unix 秒级时间戳。
| 字段 | 含义 |
|---|---|
lid | 许可证 ID。pid 和 pln 分别是产品 ID 和套餐 ID。 |
sts | 签发 token 时的许可证状态。 |
did | 签发 token 时对应的 identifier。 |
ftr | 套餐包含的功能,和 verify 返回的一致。 |
iat | token 的签发时间。exp 是应用下次应该联网校验的时间:从现在起算的联网校验间隔,或者许可证到期时间加宽限天数,取两者中较早的一个。 |
vun | 许可证本身的到期时间。没有这个字段表示永不过期。 |
grc | vun 之后的宽限天数。 |
upd | 更新期的结束时间,只有带更新期的永久许可证才有。只安装在此之前发布的版本。 |
完整流程
- token 有效且
exp还没到,就正常运行。 exp已过,就调用 verify。成功后保存新的 token,继续运行。- verify 连不上服务器时,要看你想做得多严格。大多数应用会先提醒几天再锁定功能,因为网络不稳定远比盗版常见。
- verify 返回 404,说明这个许可证在这台设备上已经不能用了。
token 的有效期就是套餐上设置的联网校验间隔。一周是比较合理的默认值;间隔越短,许可证取消后生效越快,间隔越长,越适合经常离线的应用。
停用
curl -X POST https://licenses.example.com/api/v1/license/deactivate \
-H "Content-Type: application/json" \
-d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","identifier":"9f2c41e07b6a"}'可以在应用设置里提供这个操作,比如放一个“停用这台电脑”按钮,停用后删除本地保存的密钥和 token。旧电脑已经不在手边的客户,可以自己在客户门户里释放名额,笔记本丢了也不用找客服。
错误处理
错误也用同样的外层结构返回,并带有一个可以用来分支判断的错误码。应用通常需要处理下面这几个:
| 状态码 | 错误码 | 给用户的提示 |
|---|---|---|
| 404 | LICENSE_NOT_FOUND | 密钥不对,或者这个许可证在这里已经不能用了。请用户重新输入密钥。 |
| 409 | ACTIVATION_LIMIT | 所有设备名额都已占满。返回结果里包含 details.current 和 details.max。引导用户去客户门户释放一个名额。 |
| 403 | LICENSE_EXPIRED、LICENSE_SUSPENDED、LICENSE_REVOKED、LICENSE_CANCELED | 许可证存在但无法使用时,activate 会返回这些错误码。把原因展示给用户。 |
| 404 | FEATURE_NOT_AVAILABLE | 这是 SaaS 产品,没有设备激活。请改用桌面或混合类型的产品。 |
| 429 | LOCKED_OUT | 这个网络的失败尝试次数太多。稍等一会儿再试。 |
完整的错误和重试规则见 API 参考。
重试
激活本身可以重复调用,但如果应用在超时后重试,请带上 Idempotency-Key 请求头。用户发起激活时生成一个随机值,这次激活的每次重试都发送同一个值。带相同 key 的重试会直接拿到第一次的结果,不会被重复处理。key 会保留 24 小时。
curl -X POST https://licenses.example.com/api/v1/license/activate \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1d8a52-3c0e-4b7a-9f2e-1a4c7d9e0b35" \
-d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","identifier":"9f2c41e07b6a"}'最后更新 2026年10月4日