文档目录▾

许可证激活

应用如何调用 activate、verify、deactivate 完成许可证激活、启动校验和换机,如何选择设备标识,以及用 Ed25519 签名的 token 离线验证许可证。

你的应用需要调用三个许可证接口:用户输入密钥时激活一次,应用启动时校验,用户换电脑时停用。三个接口都不需要 API 密钥,许可证密钥本身就是凭证,所以应用里没有需要藏起来的机密。

三个接口都接收 JSON,路径都在你的 Keygate 地址下的 /api/v1/license。具体框架的完整代码见 Electron、Tauri、.NET、Python 和 Swift 的接入指南。

工作原理

Keygate 为什么用随机密钥加签名 token,而不是让应用自己校验密钥,见 How license keys work(英文)。

  1. 用户输入许可证密钥后,应用带上密钥和设备标识调用 activate。Keygate 记录这次激活,并返回一个签名 token。
  2. 应用保存密钥和 token,每次启动时在本地检查 token。只要 token 还在联网校验间隔内,应用就直接运行,完全不用联网。
  3. token 到期后,应用调用 verify,拿到新的 token,以及当前的套餐和功能。如果这期间许可证被取消或吊销,返回结果里会说明。
  4. 用户换电脑时,应用调用 deactivate,空出来的名额就可以给新机器用。

选择设备标识

identifier 告诉 Keygate 这次激活属于哪台设备。同一台机器的值必须保持不变,否则每次重启都会被当成新设备,白白占掉一个名额。比较好的来源是操作系统自带的机器 ID:macOS 上的 IOPlatformUUID,Windows 注册表里的 MachineGuid,Linux 上的 /etc/machine-id。发送前先做一次哈希是个好习惯,因为你只需要这个值稳定,不需要它可读。

如果你按人授权而不是按机器授权,就把用户的邮箱作为 identifier 发送。激活只适用于桌面和混合类型的产品,SaaS 产品按席位计算人数。

激活

bash
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否客户在门户里能认出来的名字,比如电脑名称。

成功时返回:

json
{
  "success": true,
  "data": {
    "status": "activated",
    "license_id": "4f0c6c1e-2a7b-4d4e-9a55-0d3b7e1f2c11",
    "token": "eyJsaWQiOiI0ZjBj...In0.Xk3m9QJ..."
  }
}

已激活的设备再次调用 activate 会返回 already_activated,不会多占名额,所以拿不准的时候随时调用都没问题。

启动时校验

bash
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"}'
json
{
  "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,它才会变。

bash
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 示例:

javascript
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 的写法:

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 返回的一致。
iattoken 的签发时间。exp 是应用下次应该联网校验的时间:从现在起算的联网校验间隔,或者许可证到期时间加宽限天数,取两者中较早的一个。
vun许可证本身的到期时间。没有这个字段表示永不过期。
grcvun 之后的宽限天数。
upd更新期的结束时间,只有带更新期的永久许可证才有。只安装在此之前发布的版本。

完整流程

  • token 有效且 exp 还没到,就正常运行。
  • exp 已过,就调用 verify。成功后保存新的 token,继续运行。
  • verify 连不上服务器时,要看你想做得多严格。大多数应用会先提醒几天再锁定功能,因为网络不稳定远比盗版常见。
  • verify 返回 404,说明这个许可证在这台设备上已经不能用了。

token 的有效期就是套餐上设置的联网校验间隔。一周是比较合理的默认值;间隔越短,许可证取消后生效越快,间隔越长,越适合经常离线的应用。

停用

bash
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。旧电脑已经不在手边的客户,可以自己在客户门户里释放名额,笔记本丢了也不用找客服。

错误处理

错误也用同样的外层结构返回,并带有一个可以用来分支判断的错误码。应用通常需要处理下面这几个:

状态码错误码给用户的提示
404LICENSE_NOT_FOUND密钥不对,或者这个许可证在这里已经不能用了。请用户重新输入密钥。
409ACTIVATION_LIMIT所有设备名额都已占满。返回结果里包含 details.current 和 details.max。引导用户去客户门户释放一个名额。
403LICENSE_EXPIRED、LICENSE_SUSPENDED、LICENSE_REVOKED、LICENSE_CANCELED许可证存在但无法使用时,activate 会返回这些错误码。把原因展示给用户。
404FEATURE_NOT_AVAILABLE这是 SaaS 产品,没有设备激活。请改用桌面或混合类型的产品。
429LOCKED_OUT这个网络的失败尝试次数太多。稍等一会儿再试。

完整的错误和重试规则见 API 参考。

重试

激活本身可以重复调用,但如果应用在超时后重试,请带上 Idempotency-Key 请求头。用户发起激活时生成一个随机值,这次激活的每次重试都发送同一个值。带相同 key 的重试会直接拿到第一次的结果,不会被重复处理。key 会保留 24 小时。

bash
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日