# SDK 使用说明 ## 服务地址与机器码 初始化 SDK 时传入服务端基础地址和机器码。机器码要求为 32 位字母数字字符串,SDK 会自动去除首尾空格并转换为大写。 客户端应保存同一台设备的机器码。授权票据绑定卡密和机器码,换用其他机器码会导致验签失败。 ## 接口流程 1. 调用登录或验证接口 `/api/card/verify`。 2. 保存响应中的 `licenseTicket`、`expireUnix` 和 `remainDays`。 3. 按业务周期调用 `/api/card/heartbeat` 更新在线状态。 4. 需要展示设备时调用 `/api/card/devices`。 5. 用户主动解绑时调用 `/api/card/unbind`。 ## 软件接入密钥(appKey) 管理后台「软件管理」可为每个软件查看/生成接入密钥。平台默认开启强制接入校验,所有软件都需配置密钥(服务端会自动补发缺失的密钥),客户端需在初始化时传入该密钥,服务端据此校验卡密归属(空间 + 软件),拒绝跨空间、跨软件的卡密验证;未携带 `appKey` 的请求会被拒绝。平台在「平台设置」关闭强制校验后回到按软件可选:仅已配置密钥的软件需传参。详细校验规则见 `docs/api-usage.md` 的「软件接入密钥(appKey)」。各语言 SDK 初始化示例见下文。 ## 离线验签工具 授权票据 `licenseTicket` 的格式为 `base64(payload) + '.' + base64(rsa-sha256 签名)`,`payload` 含 `ch`(卡密哈希)、`mc`(机器码)、`exp`(到期 Unix 秒)、`ts`(签发 Unix 秒)。部署方提供与签名私钥配套的 RSA 公钥(`public.pem`)即可在不联网的情况下校验票据签名与字段。 ### 网页工具 访问 `https://<授权服务域名>/verify` 打开“授权票据离线验签工具”。页面完全在浏览器本地完成解析与验签,不发起网络请求;也可将 `public/verify-tool.html` 另存为本地文件后直接打开使用。输入票据、粘贴或选择 `public.pem`,可选填写机器码与参照时间,即可得到签名结论与字段明细。环境支持 WebCrypto 时使用系统密码学库,否则回退到内置纯 JS 验签实现。 ### 命令行工具 ```bash # 用票据文件与公钥文件验签,并校验机器码 php bin/verify-ticket.php --ticket-file=ticket.txt --key-file=storage/keys/public.pem --machine=A1B2C3D4E5F60718293A4B5C6D7E8F90 # 以 JSON 输出,便于脚本解析 php bin/verify-ticket.php --ticket="" --public-key="$(cat public.pem)" --json ``` 退出码:0 表示验签通过;1 表示验签不通过或无法验签;2 表示用法或文件错误。省略公钥时仅解析载荷并返回退出码 1,用于快速查看票据字段。 离线验签只能证明票据由对应私钥签发且未被篡改。服务端的冻结、解绑、卡密状态等动态信息无法离线获知,涉及授权状态的最终判定仍应调用服务端接口。 ## Python 文件:`bindings/python/license_sdk.py` ```python from license_sdk import LicenseClient client = LicenseClient('https://license.example.com', '0123456789abcdef0123456789abcdef', app_key='APP_KEY') login = client.login('CARD_KEY', '办公电脑') if login.get('ok'): print(login['licenseTicket']) heartbeat = client.heartbeat('CARD_KEY') devices = client.list_devices('CARD_KEY') client.unbind('CARD_KEY') ``` ## C# 文件:`bindings/csharp/LicenseClient.cs`,工程 `bindings/csharp/LicenseSdk.csproj`,目标 .NET 6,输出程序集名 `LicenseSdk`。 完整使用说明见 [C#/.NET DLL 使用说明](dotnet-sdk-usage.md)。 编译 DLL: ```bash cd bindings/csharp && dotnet build -c Release # 产物:bindings/csharp/bin/Release/net6.0/LicenseSdk.dll ``` 引用方式三选一: - 工程引用:在目标 .csproj 中加 ``; - 直接引用 DLL:`.../LicenseSdk.dll`; - NuGet:`dotnet pack` 生成 `LicenseSdk.5.0.0.nupkg` 后本地源安装。 SDK 提供两套等价方法:`LoginAsync` / `HeartbeatAsync` / `ListDevicesAsync` / `UnbindAsync` 返回 JSON 文档(非 2xx 抛异常);带 `ResponseAsync` 后缀的同名方法返回 `ApiResponse`(含 HTTP 状态码与原始响应体,不抛异常,便于展示服务端业务错误)。 ```csharp using System.Net.Http; using LicenseSdk; using var http = new HttpClient { BaseAddress = new Uri("https://license.example.com") }; var client = new LicenseClient(http, "0123456789abcdef0123456789abcdef", "APP_KEY"); var result = await client.LoginAsync("CARD_KEY", "办公电脑"); var heartbeat = await client.HeartbeatAsync("CARD_KEY"); // 需要读取业务错误原因时使用 ResponseAsync 系列 var resp = await client.LoginResponseAsync("CARD_KEY", "办公电脑"); if (resp.OkFlag) { /* resp.Doc 含 licenseTicket / expireUnix / remainDays */ } ``` ## Java 文件:`bindings/java/LicenseClient.java`,要求 Java 11 或更高版本。 ```java import license.sdk.LicenseClient; LicenseClient client = new LicenseClient( "https://license.example.com", "0123456789abcdef0123456789abcdef", "APP_KEY" ); LicenseClient.ApiResponse login = client.loginResponse("CARD_KEY", "办公电脑"); if (login.statusCode() == 200) { System.out.println(login.body()); } LicenseClient.ApiResponse heartbeat = client.heartbeatResponse("CARD_KEY"); ``` Java SDK 的原有方法返回 JSON 字符串;带 `Response` 后缀的方法返回 HTTP 状态码和 JSON 响应体。接入方负责解析 `ok`、`licenseTicket`、`expireUnix` 和 `remainDays`。 ## C++ 文件:`bindings/cpp/license_client.hpp` 和 `sdk-core/include/license_sdk.h`。 ```cpp #include "license_client.hpp" license_sdk::LicenseClient client("0123456789abcdef0123456789abcdef", "APP_KEY"); int status = 0; int64_t expire_unix = 0; char message[256] = {}; client.login("CARD_KEY", "办公电脑", &status, &expire_unix, message, sizeof(message)); ``` C++ 封装与底层 C ABI 当前定义了登录、设备列表和心跳方法,并透传软件接入密钥 `appKey`(`login`/`list_devices`/`heartbeat` 内部走 `_ex2` 变体)。C ABI 的 HTTP 传输由平台绑定接入;接入前应实现 `sdk-core/src/card_flow.c` 中的传输层,将 `app_key` 作为请求体 `appKey` 字段发送,并传入当前设备的机器码。Python、C# 和 Java 绑定可直接调用 HTTP API。 ## PHP 文件:`bindings/php/LicenseClient.php`,要求 PHP 7.4+,优先使用 curl 扩展,未安装 curl 时自动回退到流式 HTTP(需 `allow_url_fopen`)。 ```php require 'LicenseClient.php'; $client = new LicenseClient('https://license.example.com', '0123456789abcdef0123456789abcdef', 10, 'APP_KEY'); $login = $client->verify('CARD_KEY', '办公电脑'); // 返回 ['httpStatus'=>int, 'body'=>json] $data = json_decode($login['body'], true); if ($login['httpStatus'] === 200 && !empty($data['ok'])) { // 保存 $data['licenseTicket'] / expireUnix / remainDays } $client->heartbeat('CARD_KEY'); $client->listDevices('CARD_KEY'); $client->unbind('CARD_KEY'); // 可选离线验签:公钥为部署方 private.pem 配套的 public.pem $payload = $client->verifyTicket($data['licenseTicket'], $publicKeyPem); ``` ## Go 文件:`bindings/go/licenseclient.go`,module 名 `licenseclient`,Go 1.16+,仅依赖标准库。在自己的工程中通过 `replace` 指向本目录,或直接拷贝两个文件到工程内。 ```go import "licenseclient" client := licenseclient.New("https://license.example.com", "0123456789abcdef0123456789abcdef", 0).WithAppKey("APP_KEY") login, err := client.Login("CARD_KEY", "办公电脑") if err == nil && login.StatusCode == 200 { // json.Unmarshal([]byte(login.Body), &data) 读取 ok / licenseTicket / expireUnix / remainDays } client.Heartbeat("CARD_KEY") client.ListDevices("CARD_KEY") client.Unbind("CARD_KEY") payload, err := client.VerifyTicket(ticket, publicKeyPem) // 可选离线验签 ``` ## H5(浏览器) 文件:`bindings/h5/license-client.js`,UMD 格式,` ``` 浏览器无法读取硬件信息,机器码为随机生成并保存在 `localStorage`,清空浏览器数据后需重新验证。跨域请求需服务端为 `/api/card/*` 配置 CORS,建议与业务页面同域部署。 ## Android(Java) 文件:`bindings/android/LicenseClient.java`(纯 Java + `HttpURLConnection`,无三方依赖);`DeviceFingerprint.java` 提供本机机器码生成工具(基于 ANDROID_ID 与设备信息哈希)。 ```java import license.sdk.android.LicenseClient; import license.sdk.android.DeviceFingerprint; String machineCode = DeviceFingerprint.resolve(context); // 建议持久保存,换装后需重新验证 LicenseClient client = new LicenseClient("https://license.example.com", machineCode, "APP_KEY"); new Thread(() -> { // 网络请求在子线程执行 LicenseClient.ApiResponse login = client.verify("CARD_KEY", "我的手机"); if (login.statusCode() == 200) { JSONObject data = new JSONObject(login.body()); // Android 自带 org.json if (data.optBoolean("ok")) { // 保存 data 中 licenseTicket / expireUnix / remainDays } } client.heartbeat("CARD_KEY"); client.listDevices("CARD_KEY"); client.unbind("CARD_KEY"); }).start(); ``` Android 主线程禁止网络请求;`verify` 返回 4xx 时正文即服务端业务错误,可直接展示其中的 `msg`。 ## 错误处理 客户端应检查响应中的 `ok` 字段和 HTTP 状态。心跳返回 `kick: true` 时,应清理本地授权状态并要求用户重新验证。常见原因包括卡密过期、卡密冻结、设备被解绑或设备被新设备顶替。