接入流程
① 后台创建应用 → 拿到 AppKey / 公钥 / AppSecret
② 上传 APK → 云端加固并自动注入 AppKey 与公钥
③ 下载加固包 → 分发给用户
④ 后台生成卡密 → 发放给用户激活
⑤ 用户激活后进入应用,运行期自动心跳校验
如果你希望自行集成(不使用加固注入),可直接在自己工程中引入 SDK,
并手动在初始化时传入 AppKey 与平台公钥。
凭证说明
| 凭证 | 用途 | 存放位置 |
AppKey | 标识应用,随请求明文传输 | 客户端(加固注入) |
平台公钥 | RSA 公钥,用于加密 AES 密钥 | 客户端(加固注入) |
AppSecret | 开放 API 的 HMAC 签名密钥 | 仅服务端 |
平台私钥 | 解密请求中的 AES 密钥 | 仅服务端 |
AppSecret 与平台私钥绝不可打包进客户端。
前者用于开发者服务端调用开放 API,后者由平台保管。
初始化
LingDunConfig config = new LingDunConfig.Builder()
.appKey("sk_xxxxxxxxxxxxxxxx")
.publicKey(PUBLIC_KEY_PEM)
.apiBase("https://api.your-domain.com/api/v1")
.build();
LingDun.init(getApplicationContext(), config);
卡密激活
LingDun.activate("ABCD-EFGH-IJKL-MNOP", new LingDunCallback() {
@Override
public void onResult(boolean ok, LicenseInfo info) {
if (ok) {
// 激活成功,info.expireAt 为到期时间
}
}
});
校验授权
LingDun.check(new LingDunCallback() {
@Override
public void onResult(boolean authorized, LicenseInfo info) {
if (authorized) {
// 授权有效,进入主流程
} else {
LingDun.showActivateActivity(MainActivity.this);
}
}
});
心跳上报
应用进入前台时调用一次 check() 即可。心跳周期由后台「应用配置」的
heartbeat_interval 下发(默认 3600 秒),长在线应用可据此定时调用
heartbeat()。心跳用于更新活跃状态、下发最新公告与更新提示。
加密方案
全部终端接口使用 RSA + AES 混合加密,单次请求流程:
- 客户端随机生成 32 字节原始密钥
ak 与随机 16 字节 IV
- 业务数据 JSON 用
AES-256-CBC 加密,密文格式为 base64(IV‖密文) → payload
ak 用平台 RSA 公钥加密(PKCS1 v1.5)→ secret(Base64)
- 服务端用私钥还原
ak,以 SHA256(ak)[0..31] 派生真实密钥后解密 payload
- 响应结果用同一个
ak 加密返回,客户端本地解密即可
RSA 为 2048 位,Android 端对应 RSA/ECB/PKCS1Padding;
AES 为 AES/CBC/PKCS7Padding。示例代码见 sdk/android/Crypto.java。
请求结构
// 外层:明文 JSON
{
"app_key": "sk_xxxxxxxxxxxxxxxx",
"secret": "<RSA-PKCS1 加密的 32 字节随机密钥, Base64>",
"payload": "<base64(IV‖AES-256-CBC 密文)>"
}
// payload 解密后的明文(接口由 URL 决定,不含 action 字段)
{
"ts": 1793091637, // 秒级时间,偏差 300s 内有效
"nonce": "a1b2c3d4e5f6", // 随机串,窗口内不可重复
"fingerprint": "<设备指纹>",
"card_no": "ABCD-EFGH-IJKL", // 仅 activate 需要
"device_info": { "model": "Pixel 8", "os_version": "14" }
}
ts 与 nonce 位于 payload 明文内部,
服务端据此做时间窗校验与窗口内去重,共同防重放。
响应结构
// 通道层成功
{
"code": 0, "msg": "ok",
"encrypted": true,
"payload": "<用同一 AES 密钥加密的结果>"
}
// payload 解密后
{
"code": 0, "msg": "授权有效",
"data": { "authorized": true, /* ... */ },
"server_time": 1793091637, "action": "check"
}
// 通道层失败(app_key 无效 / 超窗 / nonce 重放 / 解密失败)
{ "code": 1001, "msg": "请求已过期或重复提交", "encrypted": false, ... }
外层 code=0 只代表加密通道建立成功,不代表授权通过。
真正的业务结果码在 payload 解密后的内层 code。
activate · 卡密激活
// POST /api/v1/activate — payload 明文
{
"ts": 1793091637, "nonce": "a1b2c3d4e5f6",
"fingerprint": "<设备指纹>",
"card_no": "ABCD-EFGH-IJKL",
"device_info": {
"model": "Pixel 8", "brand": "Google",
"os_version": "14", "sdk_version": 34
}
}
// 解密后响应 data
{
"authorized": true,
"license_type": "card",
"license_type_text": "卡密授权",
"is_permanent": false,
"expire_time": 1795683637,
"expire_time_str": "2026-11-26 17:40:37",
"days_left": 30,
"token": "<会话令牌>",
"heartbeat_interval": 3600,
"offline_grace_hours": 72,
"notice": null,
"update": { "has_update": false, "force": false }
}
同一设备再次激活新卡密时为续期累加,到期时间叠加,不会覆盖首次激活的卡密。
heartbeat · 心跳上报
// POST /api/v1/heartbeat — payload 明文
{
"ts": 1793091637, "nonce": "a1b2c3d4e5f6",
"fingerprint": "<设备指纹>"
}
// 解密后响应 data(结构与 activate 一致,附带公告与更新)
{
"authorized": true,
"expire_time": 1795683637, "days_left": 30,
"server_time": 1793091637,
"notice": { "id": 3, "title": "版本更新", "type": "dialog" },
"update": { "has_update": true, "force": false }
}
check · 校验授权
// POST /api/v1/check — payload 明文(同 heartbeat)
{ "ts": 1793091637, "nonce": "...", "fingerprint": "..." }
// 未授权时
{ "code": 2001, "msg": "未授权", "data": { "authorized": false } }
另有明文探测接口 GET /api/v1/ping,用于排查网络连通性,
返回 {"code":0,"msg":"pong","data":{"server_time":...,"version":"1.0.0"}}。
结果码
| code | 含义 | 客户端建议 |
0 | 成功 | 正常放行 |
1001 | 参数错误 / 时间窗超期 / nonce 重复 | 检查参数并校准设备时间 |
1002 | 签名校验失败 | 检查 app_secret 与排序算法 |
1003 | AppKey 不存在 | 提示联系开发者 |
1004 | 应用已停用 | 提示联系开发者 |
2001 | 未授权 | 弹出激活界面 |
2002 | 授权已过期 | 引导续费 |
2003 | 设备数超出限制 | 提示先解绑其他设备 |
2004 | 卡密不存在或已失效 | 提示卡密错误 |
2005 | 卡密已被使用 | 提示已激活 |
2006 | 卡密已过期 | 提示已过期 |
2007 | 设备已被封禁 | 提示违规并退出 |
2008 | 请求无效或已重复 | 重新生成 nonce |
2009 | 试用权限已使用 | 提示输入卡密激活 |
5000 | 服务器内部错误 | 稍后重试 |