01授权怎么跑起来的
先说清三方关系,后面所有步骤都是它的展开。AuthLink 由三个角色组成, 各司其职,不需要你改造站点架构,只要在合适的位置放一个「授权判定」的调用。
授权服务端
你部署的 AuthLink 服务端。持有主密钥,负责签发授权码、判定激活/查询/心跳、 签发加密凭据。判定逻辑全在这里,站点改不了。
你的站点
要接入授权的网站。只保存两样东西:app_key(服务器端)与
license.dat(加密凭据)。本地只做「能不能解密 + 域名对不对 + 有没有过期」。
浏览器 / 访客
只看到提示层与后台授权卡片。永远接触不到 app_key,也拿不到授权码—— 签名与解密全部在站点服务器进程内完成。
核心凭据 license.dat 是什么
站点首次激活成功后,服务端会签发一份 AES-256-GCM 加密的凭据,
站点把它原样存成 license.dat(建议 0640 权限)。它有两条硬性质:
- 密钥只由 app_key 派生(HKDF-SHA256 ⇒
key_license), 所以站点不需要主密钥也能自校验。 - 域名被绑进认证标签(AAD =
"license.dat|" + 域名)。 换个域名、或改动密文任何一个字节,解密都会直接失败 —— 复制到别的站等于一张废纸。
三个你一定会用到的服务端接口
| 接口 | 作用 | 是否需签名 |
|---|---|---|
POST /api/license/activate | 拿授权码换取 license_b64(首次激活) | 是 |
GET /api/license/info | 查询某域名的授权状态(后台「查询授权」用) | 是 |
GET /api/license/heartbeat | 心跳续期确认,连续 3 次失败服务端标记异常 | 是 |
「需签名」= 请求要带 sign、timestamp、nonce 三个字段,
算法见 5.2 节。PHP 站点不用自己算 —— SDK 与同源代理都替你算好了。
02接入前置准备
动手前,先确认这三样东西已经在你手上。
- 一个已部署的 AuthLink 服务端
按《安装部署.md》跑起来,能访问
https://你的域名/healthz并返回{"code":0,"data":{"status":"ok",...}}。安装向导最后会下发一个app_key。 - 安装向导下发的 app_key(ak_ 开头) 这是本站点的签名密钥,只放在站点服务器上,绝不能进前端、绝不能提交到 Git。 丢失它=本站点再也无法签名,只能回服务端重新生成。
- 站点对外域名(或固定公网 IP)
授权是绑定身份的:按域名授权填
blog.example.com;按 IP 授权填203.0.113.24。 域名会被归一化(剥协议/端口/www、转小写),子域自动匹配主域名。
activate 会严格校验。开发阶段用 localhost 或内网 IP 试通,
上线换正式域名时按正式域名重新领一张码即可,不要指望改 AAD 能复用旧码。03授权码从哪来(三种方式)
授权码形如 CXXX-XXXX-XXXX-XXXX,是把某个域名/IP 与你的产品关联起来的一次性凭据。
三种获取方式按角色选用。
① 用户中心自助领取
注册账户 → 进「用户中心」→ 选「按域名授权」或「按 IP 授权」→ 免费领取。 适合分发给最终站长,受单账户配额限制(默认 3 张)。
② 管理端签发
管理员在管理端直接为指定域名签发/管理授权。适合你作为作者统一控制、 批量给客户开授权。
③ 离线签发
客户服务器无法出网时用。管理端「离线签发」生成一段 license_b64,
客户在后台授权面板「离线导入」粘贴即完成。全程零网络请求。
方式 ③ 的产物与在线激活完全同构
离线签发刻意复用了同一套 license.dat 格式(同密钥、同 AAD),
所以客户端校验代码一行都不用改。唯一的区别是凭据由管理员手工传递,
而不是经由 /api/license/activate 在线换取。安全性也没打折:
离线凭据的 AAD 仍然绑定域名,复制到别的域名必然解密失败。
04方式一:PHP 站点接入(复制即用)
AuthLink 自带一个单文件、零 composer 依赖的 PHP SDK。
如果你的站点是 PHP(EMLOG / WordPress / 自研都行),照下面六步做完即可。整套逻辑由
license-client.php(SDK)+ auth-proxy.php(同源签名代理)
两个文件承担,你不需要自己写签名和 AES 代码。
4.1 复制四个文件到站点
把服务端安装包 client/ 目录里的文件复制到站点可写目录(示例放在
站点根下的 auth/ 与站点根):
你的站点/
├── auth/
│ └── license-client.php ← client/license-client.php # SDK(签名 / 解密 / 面板)
├── auth-proxy.php ← client/auth-proxy.php # 同源签名代理(浏览器调它)
├── auth-panel.css ← client/assets/auth-panel.css # 授权面板 + 前台提示层样式
└── auth-panel.js ← client/assets/auth-panel.js # 授权面板 + 心跳/提示层逻辑
auth-panel.css / auth-panel.js 的文件名与位置不可改 ——
SDK 的 siteAssets() 以相对路径引用它们,且必须放在站点可公开访问的目录。
其余文件建议放进 auth/ 并禁止外部访问(见 七、安全须知)。4.2 写站点配置 config.local.php
把 client/config.example.php 复制成 auth-proxy.php 同目录下的
config.local.php,填入三项:
<?php
return [
'server_url' => 'https://auth.example.com', // 你的 AuthLink 服务端地址(不带末尾斜杠)
'app_key' => 'ak_安装向导下发的密钥', // ★ 只放服务器端,绝不进前端
'dat_file' => __DIR__ . '/storage/license.dat', // 凭据保存路径(建议移出 web 根)
'product' => 'default', // 产品标识,与你领取授权码时填的一致即可
];
config.local.php 含明文密钥,必须让 web 服务器拒绝外部访问
(nginx deny all; 或 Apache <Files> 规则)。
4.3 站点入口引入 SDK
在站点每个 PHP 请求都会经过的公共入口(EMLOG 是 header.php;
其它框架就是你的 bootstrap / 中间件)加一行:
<?php
require_once __DIR__ . '/auth/license-client.php';
LH_Client::init([
'server' => 'https://auth.example.com',
'app_key' => 'ak_安装向导下发的密钥',
'dat_file' => __DIR__ . '/storage/license.dat',
'product' => 'default',
'claim_url'=> 'https://auth.example.com/user.html', // 未授权时「免费领取」按钮指向
]);
init() 里直接填密钥,也可以只 require 一个读取
config.local.php 的引导片段。原则只有一个:密钥只出现在服务器端 PHP 文件里。4.4 前台输出授权资源
在站点每个前台页面的公共页脚(EMLOG 是 footer.php,放在其它插件脚本
之前)输出一次资源引用与提示容器:
<?php echo LH_Client::siteAssets(); ?>
siteAssets() 只输出三样东西:
<link auth-panel.css>、<script auth-panel.js defer>、
<div id="auth-hint">。它不含任何授权详情,
前台 JS 也不会渲染授权码 / 域名 / 到期时间 —— 这些只在管理员后台能看到。
4.5 后台挂「授权设置」面板
在站点后台的设置页(EMLOG 模板是 options.php)挂入授权面板的承载项,
让管理员能看到授权卡片并输入授权码。核心是输出一段包含
LH_Client::boot() 的 HTML:
<?php
// 放在后台设置页的某个分组下(EMLOG 模板:$options 数组的一个承载项)
echo '<link rel="stylesheet" href="auth-panel.css">'
. '<script src="auth-panel.js" defer></script>'
. LH_Client::boot();
/admin/),相对路径要相应写成
../auth-panel.css。EMLOG 的模板承载项细节(分组导航、无 name 输入框的结构性保证)
见《PureBlog对接.md》第三节。4.6 激活 + 心跳
管理端进「授权设置」,粘贴授权码点「立即授权」,SDK 会完成 签名 → 请求 activate → 解密 license_b64 → 校验域名 → 写 license.dat 整条链路。 之后本地判定用:
<?php
// 本地判定:能解密 + 域名匹配 + 未过期(无网络请求,毫秒级)
if (!LH_Client::isLicensed()) {
// 未授权 —— 可渲染阻断页、隐藏付费内容、或仅提示
echo LH_Client::blockGuard(); // 输出阻断页/提示层
}
// 心跳:建议用 crontab 每小时执行一次(不阻塞页面请求)
// 0 * * * * php /path/to/站点/cron-heartbeat.php
LH_Client::heartbeat();
auth-panel.js)本身就会在页面加载时自动心跳一次,
服务端侧再配一个每小时 crontab 即可双保险。4.7 方式一验收
- 后台设置页出现授权卡片,初始显示「❌ 未授权」。
- 填入有效授权码点「立即授权」,卡片变为「✅ 已授权」,授权时间 / 域名 / 到期时间 / 指纹四项齐全。
- 数据库里查不到授权码(授权码输入框故意没有
name,结构性不落库)。 - 前台源码里搜不到授权码 / 域名 / 到期时间 / 指纹。
- 站点根出现
storage/license.dat,权限为 0640。 - 把 license.dat 复制到另一个域名,
isLicensed()返回 false(AAD 不匹配)。
05方式二:任意语言接入(HTTP API)
站点不是 PHP?完全没问题 —— 授权协议本身就是纯 HTTP + 标准密码学原语。 Node / Python / Go / Java / .NET / 甚至一个 shell 脚本,只要能发 HTTPS 请求、能做 HMAC-SHA256、能解 AES-256-GCM,就能接入。本节给出完整算法与可运行的参考实现。
5.1 你需要实现的四件事
规范化 JSON
把业务参数按键名递归字典序排序后序列化为紧凑 JSON(见 5.2)。
HMAC 签名
用 app_key 对 payload 做 HMAC-SHA256,取 hex 小写。
调三个接口
activate / info / heartbeat,带上签名三件套。
解密凭据
HKDF 派生密钥 → AES-256-GCM 解密(AAD 带域名)→ 存成 license.dat。
5.2 请求签名算法
签名覆盖「方法 + 路径 + 时间戳 + 一次性随机数 + 业务参数摘要」五项, 业务参数里不含 sign / timestamp / nonce 本身。服务端会把你发来的业务字段原样重新规范化后验签。
canonical = canonicalJson(业务参数) # 键名递归字典序排序,紧凑 JSON,不转义斜杠/Unicode
bodyHash = sha256_hex(canonical)
payload = METHOD + "\n" + PATH + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash
sign = hmac_sha256_hex(app_key, payload)
# 规则:
# METHOD 大写("POST" / "GET")
# PATH 不含查询串的路径,如 "/api/license/activate"
# timestamp Unix 秒字符串,服务端允许 ±300 秒
# nonce 一次性随机串(长度 ≤ 64),服务端内存 + 落库双重去重防重放
# 签名失败 → HTTP 403 + code 1007
| 接口 | 方法 / 路径 | 业务参数(参与签名) | 参数位置 |
|---|---|---|---|
| 激活 | POST /api/license/activate |
license_code、domain、client_ip | JSON body |
| 查询 | GET /api/license/info |
domain(可选 ip、version、integrity) | query string |
| 心跳 | GET /api/license/heartbeat |
domain(可选 ip、version、integrity) | query string |
client_ip 用于 ip_limit 校验与首次 IP 绑定;
非 IP 授权场景可传空串。info/heartbeat 的 ip 可选,用于让按 IP 绑定的授权
通过 IP 回退匹配。version / integrity 同样是可选(v1.4.0):不传就不会被判篡改,
传了才参与服务端权威指纹比对(见第 06 节)。5.3 参考实现:Node.js
import crypto from 'node:crypto';
const SERVER = 'https://auth.example.com';
const APP_KEY = process.env.AUTHLINK_APP_KEY; // ak_xxx,只放服务器端
/* 1) 规范化 JSON:键名递归字典序 */
function canonicalJson(v) {
if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']';
if (v && typeof v === 'object') {
return '{' + Object.keys(v).sort()
.map(k => JSON.stringify(k) + ':' + canonicalJson(v[k])).join(',') + '}';
}
return JSON.stringify(v);
}
/* 2) 带签名请求 */
function signParams(method, path, params) {
const ts = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString('hex');
const bodyHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
const payload = [method.toUpperCase(), path, ts, nonce, bodyHash].join('\n');
const sign = crypto.createHmac('sha256', APP_KEY).update(payload).digest('hex');
return { ...params, sign, timestamp: ts, nonce };
}
/* 3) 激活:换取 license_b64 */
async function activate(licenseCode, domain, clientIp = '') {
const path = '/api/license/activate';
const body = signParams('POST', path, {
license_code: licenseCode,
domain,
client_ip: clientIp,
});
const res = await fetch(SERVER + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.code !== 0) throw new Error(json.message); // 业务失败 HTTP 200 + code≠0
// 4) 解密并落盘
const payload = decryptLicense(json.data.license_b64, domain);
return payload;
}
/* 4) 解密 license.dat */
function deriveLicenseKey(appKey) {
return Buffer.from(crypto.hkdfSync(
'sha256', Buffer.from(appKey),
Buffer.from('AuthLink.license.v1'), Buffer.from('license.dat'), 32));
}
function b64u(s) { return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); }
function decryptLicense(b64, domain) {
const [ver, iv, tag, ct] = b64.split('.');
if (ver !== 'v1') throw new Error('凭据版本不支持');
const d = crypto.createDecipheriv('aes-256-gcm', deriveLicenseKey(APP_KEY), b64u(iv));
d.setAuthTag(b64u(tag));
d.setAAD(Buffer.from('license.dat|' + domain, 'utf8')); // ★ AAD 绑定域名
return JSON.parse(Buffer.concat([d.update(b64u(ct)), d.final()]).toString('utf8'));
}
/* 用法 */
const info = await activate('CXXX-XXXX-XXXX-XXXX', 'blog.example.com');
console.log('授权到期:', info.expire_time, '指纹:', info.fingerprint);
5.4 参考实现:Python
签名只需标准库;解密 AES-256-GCM 需要第三方库
pip install cryptography(这是唯一的外部依赖)。
import base64, hashlib, hmac, json, os, secrets, time, urllib.request
SERVER = "https://auth.example.com"
APP_KEY = os.environ["AUTHLINK_APP_KEY"]
def canonical_json(obj):
# 键名递归字典序 + 紧凑分隔符;ensure_ascii=False 对齐 PHP 的 UNESCAPED_UNICODE
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def signed_params(method, path, params):
ts = str(int(time.time()))
nonce = secrets.token_hex(16)
body_hash = hashlib.sha256(canonical_json(params).encode()).hexdigest()
payload = "\n".join([method.upper(), path, ts, nonce, body_hash])
sign = hmac.new(APP_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest()
return {**params, "sign": sign, "timestamp": ts, "nonce": nonce}
def _post_json(path, body):
req = urllib.request.Request(
SERVER + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=8) as r:
return json.loads(r.read().decode())
# ---------- 解密 license.dat ----------
def _b64u(s):
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def derive_license_key(app_key: str) -> bytes:
# HKDF-SHA256(ikm=app_key, salt="AuthLink.license.v1", info="license.dat", len=32)
prk = hmac.new(b"AuthLink.license.v1", app_key.encode(), hashlib.sha256).digest()
t = okm = b""; i = 1
while len(okm) < 32:
t = hmac.new(prk, t + b"license.dat" + bytes([i]), hashlib.sha256).digest()
okm += t; i += 1
return okm[:32]
def decrypt_license(b64: str, domain: str) -> dict:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
ver, iv_b, tag_b, ct_b = b64.split(".")
assert ver == "v1", "凭据版本不支持"
aes = AESGCM(derive_license_key(APP_KEY))
plain = aes.decrypt(_b64u(iv_b), _b64u(ct_b) + _b64u(tag_b),
("license.dat|" + domain).encode())
return json.loads(plain)
# ---------- 激活 ----------
def activate(license_code: str, domain: str, client_ip: str = "") -> dict:
path = "/api/license/activate"
body = signed_params("POST", path, {
"license_code": license_code, "domain": domain, "client_ip": client_ip})
resp = _post_json(path, body)
if resp["code"] != 0:
raise RuntimeError(resp["message"])
return decrypt_license(resp["data"]["license_b64"], domain)
if __name__ == "__main__":
info = activate("CXXX-XXXX-XXXX-XXXX", "blog.example.com")
print("已授权:", info["domain"], "到期", info["expire_time"])
5.5 之后要做的两件事
- 落盘凭据:把解密成功的明文 JSON 与原始
license_b64存下来(原始密文写license.dat,0640 权限)。本地判定时只需重新decryptLicense(读到的 b64, 当前域名)—— 能解出、域名对、expire_time大于当前时间,即为已授权。 - 起一个心跳定时任务:每小时调用一次
GET /api/license/heartbeat(同样带签名)。服务端连续 3 次收不到或返回异常, 会把该授权标记为异常(status=2)。
auth-proxy.php 当作一个「本地签名小服务」——它已封装好签名与落盘,
你只要 POST auth-proxy.php?action=activate 即可,浏览器永远看不到 app_key。06状态码语义速查
所有接口统一返回 {"code":0,"message":"...","data":...}。
业务失败一律 HTTP 200 + 非 0 code;只有签名失败用 403、
限流用 429、服务器错误用 500。接入时请按 code 判分支,
不要按 HTTP 状态判。
| code | 含义 | 接入侧建议 |
|---|---|---|
| 0 | 成功 | 正常处理 data |
| 1001 | 参数错误 | 检查字段名 / 值 |
| 1002 | 用户名或密码错误 | (登录相关) |
| 1003 / 1004 | 用户名 / 邮箱已存在 | (注册相关) |
| 1005 | 账号禁用 | (登录相关) |
| 1006 | 未登录 / 令牌失效 | (需登录的接口) |
| 1007 | 权限不足 / 签名失败 | 签名失败是 HTTP 403 + 1007:核对 app_key、时间戳 ±300s、canonical 规则 |
| 1010 | 邮箱验证码不正确或已过期 | (注册相关) |
| 1011 | 邮件服务未配置 | (管理端未填 SMTP) |
| 1012 | 验证码获取过于频繁 | (注册相关) |
| 1013 | 验证码发送失败 | (注册相关) |
| 2001 | 授权码不存在 | 吊销:删除本地 license.dat |
| 2002 | 已禁用 | 吊销:删除本地 license.dat |
| 2003 | 已过期 | 本地已按 expire_time 判 false,不删凭据 |
| 2004 | 域名不匹配 | 可能换域名,不删凭据 |
| 2005 | IP 不匹配 | 核对 ip_limit 与 client_ip |
| 2006 | 心跳异常(连续失败 3 次被标记) | 管理员改回 status=1 或重新激活 |
| 2007 | 授权码已被其它域名占用 | 按绑定域名重新领码 |
| 2008 | 超出领取配额 | (领取授权码相关) |
| 2009 | 已被拉黑 | 优先于一切授权状态;SDK 渲染拉黑版阻断页 |
| 2010 | 文件完整性校验失败(疑似破解版) | 优先于授权状态;按「被篡改」处置,别当未授权 |
| 2011 | 恢复凭据签发失败 | (管理端签发相关) |
| 9999 | 内部错误 | 按「连接故障」处理(含联不上服务器) |
license.dat 仍能解密,
也必须立即拦下。拉黑由管理员显式处置,独立于授权状态机,
接入方不该把它和「未授权」混为一谈。可选上报:在心跳 / info 的签名参数里附带
version(你的版本号)与 integrity(本地实算整包指纹,
64 位 sha256 hex)即可接入这一层;不上报不会被判篡改(未登记版本一律放行)。处置边界(v1.4.1 明确):收到 2010 只应阻断前台(写标记 + 提示), 绝不能删除本地
license.dat ——「文件疑似被改」与「授权是否有效」
是两套独立语义,删凭据会把「疑似破解」直接变成「已授权站点变未授权」,
且因文件一直不一致而永远无法自愈。另外:站点自己的配置文件
(如 config.local.php:server_url / app_key)不属于受控文件,
站长修改它不该被判定为篡改。code 落在 2000~2999 就算未授权」,
结果 2009 / 2010 / 2011 这些与授权无关的错误,也弹出了「本站未授权,
请输入授权码完成激活」,把站长引向重新激活这个无效动作。正确判据只有
一处来源:code ∈ {2001,2002,2003,2004,2005,2006,2007,2008}
才叫未授权;其余 code 各走各的专属处置(见上表)。product(v1.4.3):服务端发布指纹表的
唯一键是「产品 + 版本」,并不是版本号单独唯一。只按 version
查会被其它产品的同名版本遮蔽(后登记那行 id 更大、先被取到)
→ 指纹自然不符 → 正常站点被判 2010 整站阻断。正确做法:
info / heartbeat 都带上 product;
服务端在无法唯一定位真值时应当放行(漏判优于误杀)。07安全须知(三条红线)
授权能否立得住,取决于下面三件事有没有做对。
红线一:app_key 永不进前端
app_key 一旦泄露到浏览器,任何人都能冒充你的站点签名。
正确做法:签名发生在站点服务器进程内(SDK 或同源代理 auth-proxy.php),
浏览器只发业务参数、拿结果。前端 JS 里不得出现 app_key 字符串。
红线二:auth/ 目录与 config.local.php 拒绝外部访问
把密钥与 SDK 放进不可直接访问的目录,并显式封死:
location ^~ /auth/ { deny all; }
location = /auth-proxy.php { allow all; } # 代理需要被浏览器访问,单独放行
location ~* config\.local\.php { deny all; }
<Files "config.local.php">
Require all denied
</Files>
红线三:license.dat 放在 web 根之外,权限 0640
凭据虽是密文,但被下载走也没有必要。放到 storage/ 一类
不对外映射的目录,权限设为 0640(SDK 写入时会自动 chmod 0640)。
08常见问题
Q:我的站点没有「后台设置页」,能接吗?
能。授权面板只是方便管理员输入授权码的一种「UI」。
你也可以完全跳过 4.5,改为在服务器上用脚本调用激活:把授权码传给
LH_Client::activate($code)(PHP)或直接走 5.3 / 5.4 的 HTTP 激活接口即可。
授权与「有没有后台界面」无关。
Q:我想接入的其实是「用户注册」,不是模板授权?
AuthLink 同时提供一套账户体系(注册 / 登录 / 令牌)。
这是与模板授权两条独立的产品线:授权走 /api/license/*(签名鉴权),
账户走 /api/auth/*(登录态鉴权)。本教程聚焦授权接入;
账户接口见服务端《API.md》第 1–3 节。
Q:签名一直返回 403 / 1007,怎么排查?
- 时间戳:服务器与本机时钟是否同步?窗口只有 ±300 秒。
- canonical:键名有没有递归排序?有没有多/少字段? JSON 是否紧凑(无多余空格)?
- PATH:必须是
/api/license/activate这种完整路径, 不含 host、不含 query。 - app_key:是否用了新装的、别站点的、或带多余空白的密钥?
- nonce:同一个 nonce 用过一次就会被判重放,必须每次新生成。
Q:授权码换了域名后失效了?
这是预期行为。授权码在领取/签发时就绑定了一个身份(域名或 IP),
换域名等于换身份。按新域名重新领一张即可。归一化规则:小写、剥协议/端口/www.、
子域自动匹配主域。
Q:客户服务器无法出网,怎么授权?
用管理端的「📴 离线签发」,把生成的 license_b64 交给客户,
客户在后台授权面板「离线导入」粘贴即可。导入过程全程零网络请求,
但凭据与在线激活同构,本地校验逻辑完全一致。
Q:本地判定通过,但服务端已经把码禁用了怎么办?
靠心跳兜底。站点每小时心跳一次,服务端若判定码已禁用/过期,
会返回对应的业务错误;SDK 收到 2001 / 2002 会
立即删除本地 license.dat(无需等过期、无需管理员手动操作)。
好消息是这不需要你写代码 —— 方式一的 SDK 已经内置。
09接入验收清单
不管走哪条路径,最终都应收敛到这份可核对的结果。
- 服务端连通:服务端
/healthz返回code:0。 - 激活成功:
activate返回code:0,data.license_b64非空。 - 凭据落盘:站点出现
license.dat,权限 0640,内容为v1.iv.tag.ct四段。 - 本地判定:
isLicensed()/ 自研等价函数返回 true。 - 搬运失效:把 license.dat 换到别的域名解密 → 失败(AAD 不匹配)。
- 篡改失效:改动密文任意一字节 → 解密失败。
- 心跳正常:
heartbeat返回code:0且含server_time。 - 吊销生效:服务端把码禁用后,心跳返回
2002,本地凭据被删除。 - 密钥不外泄:站点前台源码 / 浏览器网络请求里搜不到
app_key。 - 凭据不落库:站点数据库中查不到授权码明文。