Unity TokenSDK 使用说明
离线包 v3:导出无需设备 ID,支持限期/永久,导入后可直接离线使用。首次成功联网时根据 SDK 配置的 Project Key 认领项目;其他副本联网后吊销该包授权。未联网副本无法即时吊销,永久副本不会自然到期。需升级 Native SDK;C# 导入方法与 C ABI 不变。offline_only=true 禁止认领联网请求。完整规则见仓库 docs/offline-v3.md。本次 Windows DLL 支持 v3,其他平台旧二进制不支持该协议,需重新构建。
本目录是 Unity 原生插件发布包,包含 C# P/Invoke 绑定、UGUI 示例、C ABI 头文件和按平台/架构分离的原生库。Unity 通过同一个逻辑库名 licsdk 调用所有平台;Rust 导出 ABI 不因平台改变。
release-manifest.json 是本次构建的真实产物清单。只有 status 为 available 的目标才可以放入 Unity 工程;not-built 表示发布机构缺少对应 Rust target、链接器或 Android NDK,不能用其他平台的库替代。
1. 添加 SDK 文件和原生库
将 LicSdkNative.cs、LicSdkClient.cs 和 LicenseDemoUI.cs 复制到 Unity 工程的 Assets 目录,并把 SDK/Plugins 下与你的 Unity Build Target 和 CPU 架构匹配的库复制到工程的 Assets/Plugins。不要把不同架构的库放进同一个插件目录。
| 目标平台/架构 | 原生库 | Unity 插件路径 | 状态 |
|---|---|---|---|
| Windows x64 | Plugins/Windows/x86_64/licsdk.dll | Assets/Plugins/x86_64/licsdk.dll | 已提供(ABI 2) |
| Windows x86 | Plugins/Windows/x86/licsdk.dll | Assets/Plugins/x86/licsdk.dll | 已提供(ABI 2) |
| macOS Apple Silicon | Plugins/macOS/liblicsdk.dylib | Assets/Plugins/macOS/liblicsdk.dylib | 已提供(ABI 2) |
| macOS Intel | Plugins/macOS/x86_64/liblicsdk.dylib | Assets/Plugins/macOS/x86_64/liblicsdk.dylib | 已提供(ABI 2) |
| Linux x64 | Plugins/Linux/x86_64/liblicsdk.so | Assets/Plugins/x86_64/liblicsdk.so | 暂不制作 |
| Linux arm64 | Plugins/Linux/arm64/liblicsdk.so | Assets/Plugins/ARM64/liblicsdk.so | 暂不制作 |
| Android arm64-v8a | Plugins/Android/arm64-v8a/liblicsdk.so | Assets/Plugins/Android/arm64-v8a/liblicsdk.so | 暂不制作 |
| Android armeabi-v7a | Plugins/Android/armeabi-v7a/liblicsdk.so | Assets/Plugins/Android/armeabi-v7a/liblicsdk.so | 暂不制作 |
| Android x86_64 | Plugins/Android/x86_64/liblicsdk.so | Assets/Plugins/Android/x86_64/liblicsdk.so | 暂不制作 |
C ABI 版本为 2。LicSdkClient 在初始化时会校验 lic_sdk_abi_version(),与原生库不一致会直接抛错,必须整体升级 C# 封装与原生库。当前发布包中的 Windows 与 macOS 库均为 ABI 2;打包脚本会优先从当前源码重新构建,并拒绝复用缺少 lic_sdk_abi_version 导出的旧库。
插件 .meta 使用 Unity 2022 认的平台键(Standalone: Win64 / Standalone: Win / Standalone: OSXUniversal),并为 64 位桌面库同时启用 Editor: Editor。若手工复制 .dll/.dylib 到工程,务必在 Plugin Inspector 中同时勾选 Editor 与对应的 Standalone 平台,否则 Play Mode 会因找不到原生库而报 DllNotFoundException: licsdk。
Web/static/assets/sdk 下的下载文件与发布包保持一致:licsdk-x64.dll、licsdk-x86.dll、liblicsdk.dylib(Apple Silicon)、liblicsdk-x64.dylib(Intel)。
在 Unity 的 Plugin Inspector 中确认平台和 CPU 设置。Android 原生库必须放在 ABI 子目录中,并在 Player Settings 中启用相同的 Android Target Architectures。macOS/Linux 的动态库也必须与 Unity 目标架构一致。
2. 生成发布包
在仓库根目录执行:
SDK/Token_rust/scripts/package-unity-release.sh
Windows 电脑可使用 PowerShell 版本,或使用同目录下的 .cmd 入口:
& .\SDK\Token_rust\scripts\package-unity-release.ps1
# 或
.\SDK\Token_rust\scripts\package-unity-release.cmd
脚本会构建已安装的 Rust targets,复用当前 Web 目录中已经存在的 Windows DLL,写入 SDK/Plugins 和 SDK/Token_sdk/release-manifest.json;随后把清单、README.md、licsdk.h 与本次可用的原生库(稳定文件名:licsdk-x64.dll、licsdk-x86.dll、liblicsdk.dylib、liblicsdk-x64.dylib、liblicsdk-x64.so、liblicsdk-arm64.so)同步到 Web/static/assets/sdk/,最后生成该目录下的 unity-tokensdk.zip。完整发布默认要求所有目标可用;在发布机只暂时检查可用目标时可使用 --allow-missing,但生成的 manifest 必须随包交付并在发布前补齐 not-built 项。
发布结束后脚本还会把同一套内容镜像到本地 Unity 联调工程 临时测试项目/Assets/Tokensdk(C# 文件、README.md、release-manifest.json、Plugins、include/licsdk.h、scripts/),方便直接在 Unity 中验证;工程不存在时只告警跳过。不需要该步骤时用 -NoTestProjectSync(bash 为 --no-test-project-sync),联调工程路径可用 TOKENSDK_TEST_PROJECT_DIR 覆盖。同步前请关闭 Unity Editor,否则 Windows 上占用中的 DLL 会导致复制失败。
release-manifest.json 的 schema_version 为 2,除 abi_version 外还包含 generated_at、每个 target 的 status 与 reason,以及 web_artifacts——下载目录中每个产物的 name、size_bytes 与 sha256(以本次真正同步的文件为准)。用户中心的“SDK 下载与说明”页直接读取它,因此清单缺失或过期时界面会明确标注“校验和未记录”。Web/static/assets/sdk/release-manifest.json 里额外带 unity-tokensdk.zip 自身的校验和;zip 内嵌的那份不可能包含自身哈希,这是有意为之。
只发布部分目标时,下载目录会保留上一次的同名库文件,而清单不会声明它们;此时下载页会把这类文件标注为“可能是上一次发布残留”。需要让下载目录与本次清单严格一致时加 --prune-web(PowerShell 为 -PruneWeb):它只删除白名单内的已知库文件名,不碰 README.md、licsdk.h 与 zip,也不做任何通配删除。
Android 需要安装 Android NDK,并设置 ANDROID_NDK_HOME 或 ANDROID_NDK_ROOT;Linux 交叉编译需要对应 Rust target 和一个能产出 ELF 的链接器。可通过 --prebuilt-dir 注入在专用构建机上生成的、已经过格式和导出符号检查的库。
当前发布不提供 Linux 与 Android 产物:清单会把 linux-x64、linux-arm64、android-* 记录为 not-built 并附具体原因(例如缺少 Rust target 或缺少 NDK linker),用户中心的下载页据此显示“未发布”并给出原因,不会给出无法下载的链接。将来需要这些目标时,在具备工具链的构建机上按 --target 构建,或用 --prebuilt-dir 注入已通过格式与导出符号检查的库即可,下载页会自动变为“已发布”。
3. 获取并填写 SDK 配置
登录用户中心的“SDK 下载与使用说明”,选择项目后取得接入参数。sdk_hmac_key 是激活密钥所属账户的专属 SDK 签名密钥(标准 Base64 编码的 32 字节密钥),不要使用其他账户的密钥,也不要默认用 Service 的全局回退密钥替代。根 CA PEM 应通过可信渠道取得并与部署的 TLS 证书链匹配。不要通过公开工单、截图或版本控制传递 HMAC 密钥。
Ed25519 公钥可从门户参数表或部署对外提供的 GET /api/sdk/public-key 响应中取得;也可由 Service 运维方提供并核对。Service URL 应填写门户参数表中的 base_url(HTTPS origin,不附加 /api),由 Web 代理转发 SDK 请求,不要将内部 Service 地址暴露给 Unity 客户端。客户端 HMAC 密钥可能被提取,不能替代服务端授权。
在 LicenseDemoUI 的 Unity Inspector 中按以下对照填写。既有字段名保持不变,新增的项目 Key 和间隔字段会传入 Native SDK 初始化 JSON:
| 门户参数 | Inspector 字段(C# 字段) | SDK 初始化字段 / 用途 |
|---|---|---|
base_url | Service Url(serviceUrl) | base_url |
sdk_hmac_key | Sdk Hmac Key(sdkHmacKey) | sdk_hmac_key,账户专属签名密钥 |
ed25519_public_key | Ed25519 Public Key(ed25519PublicKey) | ed25519_public_key |
pinned_root_certificate_pem | Pinned Root Certificate Pem(pinnedRootCertificatePem) | 自动转换为 pinned_root_certificates_pem 数组 |
project_key | Project Key(projectKey) | project_key,登记设备项目归属 |
heartbeat_interval_secs | Heartbeat Interval Secs(heartbeatIntervalSecs) | heartbeat_interval_secs,默认 300 秒 |
verify_interval_secs | Verify Interval Secs(verifyIntervalSecs) | verify_interval_secs,默认 14400 秒 |
license_signing_key_id | 无需填写 | 仅供核对签名密钥标识,不是 product_id |
Project Name 只是可选展示名称,不能代替 Project Key。Product Id 是保留的可选兼容字段,不要填入账户 ID、项目 Key 或签名密钥 ID。激活密钥在运行后的输入框中填写,不是项目 Key 或 HMAC 密钥。cache_path 由示例使用应用持久化目录自动生成;设备标识默认自动获取,无需填写 Device Id Override。
在线接入保持 Offline Only 关闭;纯离线部署才启用它。HTTPS 必须配置有效根 CA;Allow Insecure Http 仅用于隔离的本地开发。间隔单位为秒,请使用门户给出的值。修改 Inspector 配置后重新运行以重新初始化 SDK。
Sdk Hmac Key
将门户提供的本账户 sdk_hmac_key 复制到 Sdk Hmac Key,并确认激活密钥属于同一账户。
该密钥用于 SDK 请求签名,不是许可证签名密钥。任何分发到 Unity 客户端的 HMAC 密钥都可能被提取,因此它不是客户端身份凭证,也不能替代服务端授权。不要将密钥写入日志、截图、公开工单或版本控制,也不要将 LICENSE_SIGNING_KEY 放入 Unity。
Ed25519 Public Key
将 GET /api/sdk/public-key 返回的 Ed25519 公钥复制到 Ed25519 Public Key。这是公开的授权票据签名验证材料,必须与签发票据的 Service 相匹配。对应的 LICENSE_SIGNING_KEY 私钥只保存在 Service。
Pinned Root Certificate Pem
将运维方通过可信渠道提供的根 CA PEM 复制到 Pinned Root Certificate Pem。不要将普通服务器叶证书误当作根证书。
SDK 不使用操作系统默认根证书库,而只信任此字段中的证书列表;信任锚错误或不完整会导致 TLS 连接失败。这是 CA 信任锚,不是叶证书指纹固定。CA 轮换期间,应先配置并验证新旧根证书,再切换服务器证书。
3.1 设备名称与“所属项目”
SDK 在激活和心跳时会上报两项仅用于展示的信息(不参与许可证身份计算):
| 字段 | 来源 | 说明 |
|---|---|---|
project | Inspector 的 Project Name → 配置 project | 所属项目名称;用户中心优先用它显示设备名称 |
machine_name | 自动读取本机名(COMPUTERNAME / HOSTNAME / hostname),可用 Machine Name Override 覆盖 | 用户中心显示为本机名称 |
用户中心显示优先级:自定义名称(备注) > 所属项目 > 本机名称 > device_id 前 12 位。用户中心和管理端都可以单独改名或填写备注,这些内容只在门户展示,不影响设备标识与授权。
设备标识 device_id 由本机指纹散列得出,与上述展示字段无关。
SDK 启动时会在有项目 key 或已保存激活密钥时登记设备:项目 key 将设备归入对应项目;仅有激活密钥时按密钥所属账户登记为未归属项目设备。首次运行且既无项目 key、也无已录入激活密钥时,SDK 没有可安全确定的账户身份;设备会在用户首次在线激活时登记,不接受客户端自行指定账户 ID。
这两个字段会在激活、每次启动自动校验、每次心跳、每次计量时上报,因此改动 Inspector 后重新 Play 会立即同步(启动自动校验先于首次心跳执行)。SDK 也提供了运行期更新接口:
await LicSdkClient.Instance.SetDeviceMetadataAsync(projectName, machineNameOverride);
// 下一个校验/心跳请求即生效;传空字符串表示清空该字段
注意:SDK 在 InitAsync 时读取一次配置,直接修改 Inspector 值而不重新 Play 不会自动生效,需要调用上面的接口或重新运行。
4. 创建并连接示例场景
- 如果工程尚未包含
UnityEngine.UI,请先安装 Unity UGUI 包。 - 创建并保存示例场景,例如
LicenseDemoScene,并在 File > Build Settings > Scenes In Build 中加入该场景。 - 添加
Canvas,并配置Canvas Scaler、Graphic Raycaster和EventSystem。 - 在 Canvas 下创建以下 UGUI 控件:
- 用于输入许可证密钥的
InputField; - 三个
Button:激活、导入离线许可证、进入游戏; - 用于显示状态和错误信息的
Text。
- 用于输入许可证密钥的
- 创建名为
TokenSDK的激活 GameObject 并添加LicSdkClient。场景中只保留一个启用状态的LicSdkClient,该对象会在场景切换时持续存在。 - 将
LicenseDemoUI添加到示例 UI GameObject,并把 InputField、三个 Button 和状态 Text 拖到对应字段。若引用缺失,示例会在 Unity Console 中报告并停用自身。 - 设置 Service URL 和上文所述的三个值。生产环境必须关闭
Allow Insecure Http。将Game Scene Name设置为受保护场景的准确名称,并将该场景加入 Scenes In Build。 - 进入 Play 模式。SDK 初始化成功前按钮会保持禁用。粘贴有效许可证密钥并点击 Activate;激活成功后点击 Enter Game,示例会执行本地 challenge/scene 检查并加载配置的场景。
示例使用 300 秒心跳间隔和 4 小时在线校验间隔。SDK 初始化且许可证激活后,由 Rust 后台任务管理这些定时操作;Unity 示例不会再启动第二个心跳定时器。Challenge/response 和场景门禁只是应用层演示,并非服务端强制的安全边界。重要操作必须由 Service 执行授权判断,不要只依赖客户端场景检查来保护数据或功能。
5. 导入离线激活包
- 请管理员或客户本人从管理端/客户门户导出目标激活密钥和设备的离线激活包。导出时密钥即被占用并绑定设备。
- 将完整的离线激活包 JSON 响应保存为
license.licbin。 - 将文件复制到目标运行环境的
Application.persistentDataPath,文件名必须保持不变。Unity Editor 下该路径因平台而异;如果文件不存在,示例会显示预期的完整路径。 - 启动示例并选择 Import Offline License。SDK 会验证签名、设备、权限和有效期,然后保存加密的本地缓存。
离线模式允许对已导入激活包执行本地检查,但仅在线功能(例如用量计量)仍需要连接 Service。导入操作不会创建离线激活包。
6. 启动自动恢复、事件回调与令牌管理
SDK 会把成功激活(或导入离线激活包)后的授权写入加密缓存 cache_path,并在下次 lic_sdk_init 时自动恢复,因此不需要用户重新输入激活密钥。
初始化后按以下顺序自动执行,并通过回调把结果通知宿主:
| 事件 | 数值 | 触发时机 | code | state |
|---|---|---|---|---|
LIC_EVENT_STARTUP | 1 | 缓存读取与本地校验完成(立即触发) | 0 表示缓存可用;否则为错误码(如 -7 无许可证、-8 过期、-9 已吊销、-17 设备已停止、-6 票据签名无效) | 当前许可证状态 |
LIC_EVENT_ONLINE_CHECK | 2 | 缓存可用且非 offline_only 时,后台自动向门户校验 | 0 表示在线校验通过;-4 表示网络不可达(此时按离线宽限继续) | 校验后的状态 |
LIC_EVENT_STATE_CHANGED | 3 | 之后的任何状态变化(心跳失败降级、后台校验发现吊销/过期、导入离线激活包等) | 0 或触发本次变化的错误码 | 新状态 |
C# 封装已经把事件切回 Unity 主线程,直接订阅即可:
LicSdkClient.Instance.LicenseEventReceived += info =>
{
Debug.Log($"{info.Type} code={info.Code} state={info.State} {info.Message}");
};
必须在 InitAsync 之前订阅;错过的事件仍可从 LastStartupResult、LastOnlineCheckResult 读取。
InitAsync 等待期间启动/在线校验事件就可能已经到达,因此不要在初始化返回后无条件把状态栏改回“正在自动校验”。如果激活密钥被拒(例如已暂停),之后不会再加发事件,占位文案会一直停住。应先读取 LastStartupResult / LastOnlineCheckResult;两者都为空时先短暂等待事件,超时后调用 GetStateAsync() 用当前状态兜底(示例见 LicenseDemoUI.ResolveWithoutEventAsync),否则界面会永远停在“已保存激活密钥,正在自动校验...”。
停止某台设备(-17)
管理员或客户本人都可以在用户中心/后台按设备执行“停止”,或对整张许可证执行“停止全部设备”:
- 该设备之后的校验、心跳、计量扣减都会返回
-17 device has been blocked,本地门禁同时关闭(状态码9); - 被停止的设备仍占用设备名额,若需要释放在名额请使用“解绑”;
- 激活一个已被停止的设备同样返回
-17,不会自动解除停止; - 缓存与令牌保留:停止只会让本地门禁关闭,不会删除本地缓存,
HasSavedLicense仍为true,VerifySavedAsync()等接口继续可用; - 恢复(取消停止)后无需用户重新输入令牌:SDK 用独立的恢复重试定时器(
clamp(heartbeat_interval_secs, 5s, 30s),默认约 30 秒一次)自动重试,成功后自动清除拒签标记并回到可用状态;也可由宿主主动调用VerifySavedAsync()立即恢复; - 只有吊销(
-9)或许可证不存在才会删除缓存并清除令牌,此时才需要重新输入许可证密钥。
SDK 侧可用 LicenseSdkError.DeviceBlocked(-17)判断该情况,并提示用户联系发行方。
连不上服务端时的本地可用性
本地缓存中的票据由 Service 用 Ed25519 签名,因此其中的 expires_at 是可信的本地截止时间:
- 有到期日的许可证:在校验服务不可达时,本地门禁持续放行到
expires_at,不再受 24 小时宽限限制; - 永久授权(
expires_at为空):离线永久可用; - 计量扣减(
ConsumeSavedAsync)等在线能力仍需连通服务端,离线时返回-13; - 系统时钟被回拨到上次成功校验之前会返回
-16(clock rollback),不会延长授权; - 若需要保留“定期上线才能继续用”的严格策略,可在 SDK 配置 JSON 中加入
"offline_policy": "grace",此时离线窗口为最后一次成功校验后的offline_grace_secs(默认 86400 秒)。
导入离线激活包与在线授权共存
ImportOfflineLicenseAsync(package)(原生 lic_import_offline_license)把离线激活包写入第二槽位,不会清除已激活的在线授权:
- 在线优先:门禁先用在线票据;在线票据不可用(无票据 / 断网超过窗口 / 已到期)时才使用离线激活包(状态
7限期、8永久); - 持有离线激活包时
VerifyAsync/HeartbeatAsync/ConsumeAsync仍然正常可用(不再返回-13); - 服务端明确拒绝优先:收到吊销
-9、暂停-7、设备停止-17时本地立即失效(状态5/9/1),永久证书也不放行;恢复后自动重试并回到可用状态; - 吊销:用户中心/后台可吊销某条导出记录,SDK 在下次在线校验(
verify/heartbeat/activate)读到offline_revoked_at后删除本地离线包槽;重新签发的新包(issued_at晚于吊销时间)不受影响。设备一直离线时无法实时失效,这是离线授权的固有限制。
对应事件表现:启动时 LIC_EVENT_STARTUP 报告 code=0, state=3(缓存可用),随后 LIC_EVENT_ONLINE_CHECK 在断网时报告 code=-4, state=3——state=3 表示授权仍然可用,宿主不应因为该错误码就阻止玩家进入。
使用已保存的令牌
LicSdkClient.HasSavedLicense/HasSavedLicenseAsync():本地是否存在可直接使用的许可证密钥(缓存可用即返回true;被停止、暂停或过期时仍为true)。许可证密钥本身不会暴露给 C#。- 被停止 / 暂停 / 过期后令牌仍然保留;恢复或续期后校验成功即自动回到可用状态,不需要用户重新输入令牌。
VerifySavedAsync()、HeartbeatSavedAsync()、ConsumeSavedAsync(metric, amount, idempotencyKey):使用缓存令牌执行,无需再传入license_key。
更新令牌
UpdateLicenseKeyAsync(newKey) 用新令牌在线激活;只有激活成功才会替换本地缓存,失败时旧许可证仍然可用,调用方可提示用户重试。示例 UI 的 Activate 按钮据此自动分流:输入框为空且有已保存令牌时改为校验缓存,输入新令牌时执行更新,没有已保存令牌时执行首次激活。
7. 运行与构建说明
- 原生接口使用 UTF-8 字节数组和显式
UIntPtr长度。C# 封装在工作任务中执行阻塞 FFI 操作,并在 Unity 同步上下文中更新 UI。 LicSdkClient也封装了心跳、计量扣减、功能门禁和核心逻辑门禁;失败时可通过GetLastErrorAsync读取 Rust SDK 返回的错误文本。初始化前调用这些操作会明确抛出InvalidOperationException。ConsumeAsync(licenseKey, metric, amount, idempotencyKey)中的幂等键可选;重试同一计量请求时应传入相同幂等键,避免重复扣减。- 在应用生命周期内保持
LicSdkClientGameObject 存活。它负责 SDK 初始化、原生后台运行时、本地缓存、心跳和关闭。 - 不要记录
sdkHmacKey、许可证密钥或本地缓存文件。分发到客户端的 HMAC 密钥可以被提取,最终授权仍由 Service 决定。 - HTTPS 连接需要
pinnedRootCertificatePem中的有效 PEM 信任锚。Allow Insecure Http仅可用于隔离的本地开发环境,发布版本绝不能开启。 - 仓库中的
临时测试项目仅用于 Windows 联调,不代表其他平台已经运行验证。正式发布前,必须在实际 Unity Editor、Mono/IL2CPP、Windows、macOS、Linux 和 Android 设备/架构上验证库加载、TLS 证书链、缓存目录、离线导入和场景加载行为。
初始化返回 -2
-2 表示 SDK 配置无效。请查看示例状态文本中冒号后的详细原因,并确认:Service URL 是门户显示的 HTTPS origin(不要附加 /api 路径);HMAC Key 与 Ed25519 公钥原样粘贴、Base64 解码后各为 32 字节;根证书字段粘贴完整的 PEM(包括 BEGIN/END CERTIFICATE 行)。SDK 会修正常见的证书头/尾换行格式,但不会接受缺失或无效证书。HTTP 仅在明确启用开发选项时允许使用。
激活返回 -5(request signature was rejected)
Service 对该请求返回了 HTTP 401。按顺序检查:
Service URL必须是门户 origin(本地开发为http://127.0.0.1:8080);生产环境必须使用 HTTPS。- Web 代理与 Service 必须按部署配置正确连接;客户端不应直接访问仅供内网使用的 Service 地址。
Sdk Hmac Key必须与激活密钥所属账户在门户显示的sdk_hmac_key相匹配。- 客户端与 Service 的系统时钟偏差不能超过 30 秒,否则请求会被判为过期。