verify('FFCD-21F1-4349-EEE1-5411-7758', '办公电脑'); * if ($login['httpStatus'] === 200) { * $data = json_decode($login['body'], true); * if (!empty($data['ok'])) { * // 保存 licenseTicket / expireUnix / remainDays 供离线授权 * } * } * $client->heartbeat('FFCD-21F1-4349-EEE1-5411-7758'); * $client->listDevices('FFCD-21F1-4349-EEE1-5411-7758'); * $client->unbind('FFCD-21F1-4349-EEE1-5411-7758'); * * 离线验签(可选):把部署方下发的 RSA 公钥传入 verifyTicket(), * 服务端 licenseTicket = base64(payload) . '.' . base64(rsa-sha256签名), * payload 含 ch(卡密哈希)/mc(机器码)/exp(到期 unix 秒)/ts(签发时间)。 */ class LicenseClient { /** @var string 服务端地址,如 https://license.example.com */ private $baseUrl; /** @var string 32 位本机机器码(自动转大写) */ private $machineCode; /** @var int 请求超时秒数 */ private $timeout; /** @var string 软件接入密钥(管理后台「软件管理」生成,为空表示不携带) */ private $appKey; public function __construct(string $baseUrl, string $machineCode, int $timeout = 10, string $appKey = '') { $this->baseUrl = rtrim($baseUrl, '/'); $this->machineCode = strtoupper(trim($machineCode)); $this->timeout = max(1, $timeout); $this->appKey = trim($appKey); } /** 携带接入密钥(启用后服务端据其校验卡密归属,拒绝跨空间、跨软件验证) */ private function scope(array $payload): array { if ($this->appKey !== '') { $payload['appKey'] = $this->appKey; } return $payload; } /** 登录/验证:校验卡密并绑定当前设备,成功返回授权票据 */ public function verify(string $cardKey, string $deviceName = null): array { return $this->post('/api/card/verify', $this->scope([ 'cardKey' => $cardKey, 'machineCode' => $this->machineCode, 'deviceName' => $deviceName === null ? '' : $deviceName, ])); } /** 心跳:按业务周期调用,维持设备在线状态并刷新到期时间 */ public function heartbeat(string $cardKey): array { return $this->post('/api/card/heartbeat', $this->scope([ 'cardKey' => $cardKey, 'machineCode' => $this->machineCode, ])); } /** 查询当前卡密已绑定的设备列表 */ public function listDevices(string $cardKey): array { return $this->post('/api/card/devices', $this->scope([ 'cardKey' => $cardKey, 'machineCode' => $this->machineCode, ])); } /** 解绑当前设备(换机或归还授权时调用) */ public function unbind(string $cardKey): array { return $this->post('/api/card/unbind', $this->scope([ 'cardKey' => $cardKey, 'machineCode' => $this->machineCode, ])); } /** * 校验授权票据:离线判断票据是否真实、机器码与有效期是否匹配。 * * @param string $ticket login/verify 返回的 licenseTicket * @param string $publicKeyPem 部署方提供的 RSA 公钥 PEM * @return array|null 校验通过返回 payload(ch/mc/exp/ts),否则返回 null */ public function verifyTicket(string $ticket, string $publicKeyPem): ?array { $pos = strrpos($ticket, '.'); if ($pos === false) { return null; } $payloadRaw = base64_decode(substr($ticket, 0, $pos), true); $signature = base64_decode(substr($ticket, $pos + 1), true); if ($payloadRaw === false || $signature === false) { return null; } $key = openssl_pkey_get_public($publicKeyPem); if ($key === false) { return null; } $ok = openssl_verify($payloadRaw, $signature, $key, OPENSSL_ALGO_SHA256); if ($ok !== 1) { return null; } $payload = json_decode($payloadRaw, true); if (!is_array($payload) || !isset($payload['mc'], $payload['exp'], $payload['ch'])) { return null; } if (strcasecmp((string)$payload['mc'], $this->machineCode) !== 0) { return null; } if ((int)$payload['exp'] < time()) { return null; } return $payload; } /** * POST JSON 并返回 ['httpStatus' => int, 'body' => string]。 * 网络层失败抛 RuntimeException;4xx/5xx 不抛异常,正文即服务端业务错误。 */ private function post(string $path, array $payload): array { $body = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($body === false) { throw new RuntimeException('授权客户端请求序列化失败'); } $url = $this->baseUrl . $path; if (function_exists('curl_init')) { return $this->postCurl($url, $body); } return $this->postStream($url, $body); } private function postCurl(string $url, string $body): array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => $this->timeout, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_SSL_VERIFYPEER => true, ]); $response = curl_exec($ch); $error = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($response === false) { throw new RuntimeException('授权服务请求失败:' . $error); } return ['httpStatus' => $status, 'body' => (string)$response]; } private function postStream(string $url, string $body): array { $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => $body, 'timeout' => $this->timeout, 'ignore_errors' => true, ], ]); $response = @file_get_contents($url, false, $context); if ($response === false) { throw new RuntimeException('授权服务请求失败(请确认服务器支持 allow_url_fopen)'); } $status = 0; if (isset($http_response_header)) { foreach ($http_response_header as $line) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $m)) { $status = (int)$m[1]; } } } return ['httpStatus' => $status, 'body' => $response]; } }