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 x64Plugins/Windows/x86_64/licsdk.dllAssets/Plugins/x86_64/licsdk.dll已提供(ABI 2)
Windows x86Plugins/Windows/x86/licsdk.dllAssets/Plugins/x86/licsdk.dll已提供(ABI 2)
macOS Apple SiliconPlugins/macOS/liblicsdk.dylibAssets/Plugins/macOS/liblicsdk.dylib已提供(ABI 2)
macOS IntelPlugins/macOS/x86_64/liblicsdk.dylibAssets/Plugins/macOS/x86_64/liblicsdk.dylib已提供(ABI 2)
Linux x64Plugins/Linux/x86_64/liblicsdk.soAssets/Plugins/x86_64/liblicsdk.so暂不制作
Linux arm64Plugins/Linux/arm64/liblicsdk.soAssets/Plugins/ARM64/liblicsdk.so暂不制作
Android arm64-v8aPlugins/Android/arm64-v8a/liblicsdk.soAssets/Plugins/Android/arm64-v8a/liblicsdk.so暂不制作
Android armeabi-v7aPlugins/Android/armeabi-v7a/liblicsdk.soAssets/Plugins/Android/armeabi-v7a/liblicsdk.so暂不制作
Android x86_64Plugins/Android/x86_64/liblicsdk.soAssets/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_urlService Url(serviceUrl)base_url
sdk_hmac_keySdk Hmac Key(sdkHmacKey)sdk_hmac_key,账户专属签名密钥
ed25519_public_keyEd25519 Public Key(ed25519PublicKey)ed25519_public_key
pinned_root_certificate_pemPinned Root Certificate Pem(pinnedRootCertificatePem)自动转换为 pinned_root_certificates_pem 数组
project_keyProject Key(projectKey)project_key,登记设备项目归属
heartbeat_interval_secsHeartbeat Interval Secs(heartbeatIntervalSecs)heartbeat_interval_secs,默认 300 秒
verify_interval_secsVerify 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 在激活和心跳时会上报两项仅用于展示的信息(不参与许可证身份计算):

字段来源说明
projectInspector 的 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. 创建并连接示例场景

  1. 如果工程尚未包含 UnityEngine.UI,请先安装 Unity UGUI 包。
  2. 创建并保存示例场景,例如 LicenseDemoScene,并在 File > Build Settings > Scenes In Build 中加入该场景。
  3. 添加 Canvas,并配置 Canvas Scaler、Graphic Raycaster 和 EventSystem。
  4. 在 Canvas 下创建以下 UGUI 控件:
    • 用于输入许可证密钥的 InputField;
    • 三个 Button:激活、导入离线许可证、进入游戏;
    • 用于显示状态和错误信息的 Text。
  5. 创建名为 TokenSDK 的激活 GameObject 并添加 LicSdkClient。场景中只保留一个启用状态的 LicSdkClient,该对象会在场景切换时持续存在。
  6. 将 LicenseDemoUI 添加到示例 UI GameObject,并把 InputField、三个 Button 和状态 Text 拖到对应字段。若引用缺失,示例会在 Unity Console 中报告并停用自身。
  7. 设置 Service URL 和上文所述的三个值。生产环境必须关闭 Allow Insecure Http。将 Game Scene Name 设置为受保护场景的准确名称,并将该场景加入 Scenes In Build。
  8. 进入 Play 模式。SDK 初始化成功前按钮会保持禁用。粘贴有效许可证密钥并点击 Activate;激活成功后点击 Enter Game,示例会执行本地 challenge/scene 检查并加载配置的场景。

示例使用 300 秒心跳间隔和 4 小时在线校验间隔。SDK 初始化且许可证激活后,由 Rust 后台任务管理这些定时操作;Unity 示例不会再启动第二个心跳定时器。Challenge/response 和场景门禁只是应用层演示,并非服务端强制的安全边界。重要操作必须由 Service 执行授权判断,不要只依赖客户端场景检查来保护数据或功能。

5. 导入离线激活包

  1. 请管理员或客户本人从管理端/客户门户导出目标激活密钥和设备的离线激活包。导出时密钥即被占用并绑定设备。
  2. 将完整的离线激活包 JSON 响应保存为 license.licbin。
  3. 将文件复制到目标运行环境的 Application.persistentDataPath,文件名必须保持不变。Unity Editor 下该路径因平台而异;如果文件不存在,示例会显示预期的完整路径。
  4. 启动示例并选择 Import Offline License。SDK 会验证签名、设备、权限和有效期,然后保存加密的本地缓存。

离线模式允许对已导入激活包执行本地检查,但仅在线功能(例如用量计量)仍需要连接 Service。导入操作不会创建离线激活包。

6. 启动自动恢复、事件回调与令牌管理

SDK 会把成功激活(或导入离线激活包)后的授权写入加密缓存 cache_path,并在下次 lic_sdk_init 时自动恢复,因此不需要用户重新输入激活密钥。

初始化后按以下顺序自动执行,并通过回调把结果通知宿主:

事件数值触发时机codestate
LIC_EVENT_STARTUP1缓存读取与本地校验完成(立即触发)0 表示缓存可用;否则为错误码(如 -7 无许可证、-8 过期、-9 已吊销、-17 设备已停止、-6 票据签名无效)当前许可证状态
LIC_EVENT_ONLINE_CHECK2缓存可用且非 offline_only 时,后台自动向门户校验0 表示在线校验通过;-4 表示网络不可达(此时按离线宽限继续)校验后的状态
LIC_EVENT_STATE_CHANGED3之后的任何状态变化(心跳失败降级、后台校验发现吊销/过期、导入离线激活包等)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)

管理员或客户本人都可以在用户中心/后台按设备执行“停止”,或对整张许可证执行“停止全部设备”:

SDK 侧可用 LicenseSdkError.DeviceBlocked(-17)判断该情况,并提示用户联系发行方。

连不上服务端时的本地可用性

本地缓存中的票据由 Service 用 Ed25519 签名,因此其中的 expires_at 是可信的本地截止时间:

导入离线激活包与在线授权共存

ImportOfflineLicenseAsync(package)(原生 lic_import_offline_license)把离线激活包写入第二槽位,不会清除已激活的在线授权:

对应事件表现:启动时 LIC_EVENT_STARTUP 报告 code=0, state=3(缓存可用),随后 LIC_EVENT_ONLINE_CHECK 在断网时报告 code=-4, state=3——state=3 表示授权仍然可用,宿主不应因为该错误码就阻止玩家进入。

使用已保存的令牌

更新令牌

UpdateLicenseKeyAsync(newKey) 用新令牌在线激活;只有激活成功才会替换本地缓存,失败时旧许可证仍然可用,调用方可提示用户重试。示例 UI 的 Activate 按钮据此自动分流:输入框为空且有已保存令牌时改为校验缓存,输入新令牌时执行更新,没有已保存令牌时执行首次激活。

7. 运行与构建说明

初始化返回 -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。按顺序检查:

  1. Service URL 必须是门户 origin(本地开发为 http://127.0.0.1:8080);生产环境必须使用 HTTPS。
  2. Web 代理与 Service 必须按部署配置正确连接;客户端不应直接访问仅供内网使用的 Service 地址。
  3. Sdk Hmac Key 必须与激活密钥所属账户在门户显示的 sdk_hmac_key 相匹配。
  4. 客户端与 Service 的系统时钟偏差不能超过 30 秒,否则请求会被判为过期。