DEVELOPER GUIDE

客户端接入文档

ThinkPHP 6 服务端 · HMAC-SHA256 请求与响应签名 · UTF-8 JSON

1. 接入准备

在管理后台「产品管理」中创建产品,取得 app_key 与 app_secret。app_secret 只应保存在你的客户端安全区域或服务端,不要上传到公开仓库。

接口根地址
https://sq.yuanan6.cn/api/v1
请求方式
POST,application/x-www-form-urlencoded
响应格式
JSON,业务处理成功时 code = 0
时间窗口
默认 300 秒,客户端需通过 NTP 校准时间
授权激活从首次成功调用 activate 开始计时。verify 与 heartbeat 都要求传入已经绑定的设备或域名。

2. 签名规则

  1. 移除 sign、sign_type 以及空值参数。
  2. 按参数名 ASCII 升序排序。
  3. 键和值分别执行 RFC 3986 rawurlencode,以 key=value 和 & 拼接。
  4. 以 app_secret 为密钥计算 HMAC-SHA256,输出小写十六进制。
$sign = hash_hmac('sha256', canonical($params), $appSecret);

所有接口还必须包含 timestamp(秒级 Unix 时间戳)和 nonce(8-64 位随机字符串)。相同 nonce 在时间窗内不能重复使用。

响应中的 sign 计算方式:移除 sign 后,将其余响应字段按原顺序用 JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES 编码,再计算 HMAC-SHA256。

3. PHP 7.4 完整示例

示例激活后执行一次验证。其他语言按相同签名规则实现即可。

<?php
$baseUrl = 'https://sq.yuanan6.cn';
$appKey = '你的 app_key';
$appSecret = '你的 app_secret';
$licenseKey = 'XL-XXXXX-XXXXX-XXXXX-XXXXX';

function canonical(array $params): string {
    unset($params['sign'], $params['sign_type']);
    $params = array_filter($params, function ($value) {
        return is_scalar($value) && $value !== '';
    });
    ksort($params, SORT_STRING);
    $pairs = [];
    foreach ($params as $key => $value) {
        $pairs[] = rawurlencode((string) $key) . '=' . rawurlencode((string) $value);
    }
    return implode('&', $pairs);
}

function apiRequest($baseUrl, $endpoint, $appKey, $appSecret, array $data) {
    $params = array_merge($data, [
        'app_key' => $appKey,
        'timestamp' => time(),
        'nonce' => bin2hex(random_bytes(16)),
    ]);
    $params['sign'] = hash_hmac('sha256', canonical($params), $appSecret);
    $ch = curl_init($baseUrl . '/api/v1/' . $endpoint);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($params),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15,
    ]);
    $raw = curl_exec($ch);
    if ($raw === false) {
        throw new RuntimeException(curl_error($ch));
    }
    curl_close($ch);
    $response = json_decode($raw, true);
    if (!is_array($response) || !isset($response['sign'])) {
        throw new RuntimeException('响应格式或签名缺失');
    }
    $sign = $response['sign'];
    unset($response['sign']);
    $json = json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if (!hash_equals(hash_hmac('sha256', $json, $appSecret), $sign)) {
        throw new RuntimeException('响应验签失败');
    }
    return $response;
}

// 设备指纹应由客户端稳定生成,不要使用 IP 或随机值。
$deviceId = hash('sha256', '稳定的设备特征');
$result = apiRequest($baseUrl, 'activate', $appKey, $appSecret, [
    'license_key' => $licenseKey,
    'device_id' => $deviceId,
]);
if ($result['code'] !== 0) {
    exit($result['message']);
}
print_r($result['data']);

4. 接口列表

接口路径业务参数用途
POST/api/v1/config仅通用参数获取版本、更新地址和公告
POST/api/v1/activatelicense_key, device_id / domain首次激活并开始计时,重复激活幂等
POST/api/v1/verifylicense_key, device_id / domain验证授权、到期与绑定状态
POST/api/v1/heartbeat同 verify验证并刷新绑定在线状态
POST/api/v1/deactivatelicense_key, binding_type, identifier解绑设备或域名,消耗解绑次数

binding_type 为 device 或 domain;identifier 为对应原始 device_id 或 domain。解绑后再次激活不会重复占用历史绑定。

5. 通用参数

参数必填说明
app_key是产品公开接入标识
timestamp是秒级 Unix 时间戳
nonce是每次请求不同的 8-64 位随机字符串
sign是HMAC-SHA256 小写签名
device_id按产品8-128 位字母、数字或 _ . : -
domain按产品纯域名或 IP,可传 URL 由服务端提取主机名

6. 常见状态码

0处理成功
40001 / 40010缺少通用参数或绑定参数
40101 / 40102 / 40103签名错误、时间过期、重复请求
40302 / 40303授权冻结或产品停用
40401授权不存在或不属于产品
41001 / 41003授权到期或尚未激活
41004 / 41005 / 41008绑定不匹配、达到上限、解绑次数用完
42901 / 50000请求限流或服务暂时不可用

7. 生产接入建议

  • 全站启用 HTTPS;客户端设置连接与读取超时,不要关闭 TLS 证书校验。
  • 心跳间隔使用响应中的 heartbeat_after,不要高频请求。
  • 验证响应签名与 product 字段,防止伪造或串用产品响应。
  • 设备指纹只保存稳定哈希,不采集无关隐私信息;解绑属于敏感操作,应由用户主动确认。
  • 网络不可用时采用适合你产品的有限宽限策略,并在恢复联网后立即验证,不要将异常一律视为授权有效。
  • 保存 request_id 用于售后定位,不要记录 app_secret 或完整支付通知密钥。