客户接入卡密验证说明

客户的软件、网页、脚本只调用验证接口,不需要也不能使用 admin_token。

接口地址

把下面域名换成你的网站域名:

https://你的域名/card_auth_api.php

管理后台用的 admin_token 只给你自己用。客户接入时不要传这个字段。

商户账号和密钥

商户打开后台页 card_admin.html 后可以自己注册账号并登录。登录后会看到自己的 admin_token 和 signing_secret。

admin_token:商户后台管理密钥,只能管理自己的卡密
signing_secret:签名密钥,用来校验 verify / heartbeat 返回是否真实

登录后可以看全部商户。普通商户登录后只能看自己的卡密、自己的日志和自己的密钥。

简单接入和加强接入

简单接入:只判断接口返回 code = 0,最快,网页、exe、ipa、apk、脚本都能用。

加强接入:请求时多传 nonce,客户端再校验返回的 data.auth.signature。这样可以提高伪造返回、抓包重放、简单改代码绕过的成本。

没有任何网页或软件授权能做到 100% 防破解。正式使用建议同时开启 HTTPS、设备绑定、运行中心跳、签名校验,软件再配合混淆或加壳。

首次验证

用户输入卡密后,客户的软件或网页请求 verify。如果是天卡、周卡、月卡、自定义卡,第一次验证成功时才开始计算到期时间。永久卡不会生成到期时间。

{
  "action": "verify",
  "code": "客户输入的卡密",
  "app": "default_app",
  "device": "客户设备码",
  "scene": "exe",
  "nonce": "随机字符串,可选"
}

返回 code = 0 表示通过。常用字段:

{
  "code": 0,
  "message": "Verify success.",
  "data": {
    "card": {
      "status": "active",
      "type": "天卡",
      "platform_text": "Windows exe",
      "duration_text": "1 天",
      "expires_at": "2026-06-07T20:00:00+08:00",
      "expires_text": "2026-06-07 20:00:00"
    },
    "auth": {
      "algorithm": "HMAC-SHA256",
      "merchant": "abc_shop",
      "nonce": "随机字符串",
      "server_time": "2026-06-06T20:00:00+08:00",
      "payload": "待签名原文",
      "signature": "服务端签名"
    }
  }
}

后台生成卡密时可以限制允许接入格式。比如只允许 exe 的卡密,用 ipa 或 apk 去验证会失败。

运行中心跳

软件验证成功后,建议每 5 到 10 分钟请求一次 heartbeat。如果返回失败,说明卡密可能到期、被禁用、换卡、设备不匹配,软件应停止授权功能。

{
  "action": "heartbeat",
  "code": "已经验证过的卡密",
  "app": "default_app",
  "device": "同一台设备码",
  "scene": "exe",
  "nonce": "每次心跳重新生成"
}

接入格式 scene 对照

客户软件请求接口时要带 scene,后台会用它判断这张卡密是否允许当前格式接入。

网页/Web:scene = "web"
Windows exe:scene = "exe"
iOS ipa:scene = "ipa"
Android app/apk/aab:scene = "apk"
脚本/插件/Python/Node:scene = "script"

软件接入流程

.exe、.ipa、.apk 只是打包后的文件格式,真正接入是在软件源码里加验证请求。所有平台流程一样:

软件里不要写入 admin_token。客户软件只需要接口地址、软件标识 app、卡密、设备码和接入格式 scene。

加强签名校验

商户登录后台复制自己的 signing_secret。软件端收到接口返回后,用 auth.payload 和 signing_secret 重新计算 HMAC-SHA256,结果等于 auth.signature 才继续运行。

校验规则:
1. 本地计算 hash_hmac_sha256(auth.payload, signing_secret)
2. 本地结果必须等于 auth.signature
3. auth.nonce 必须等于本次请求发出去的 nonce
4. auth.server_time 和本机时间相差建议不超过 2 到 5 分钟

网页前端不要直接写 signing_secret,因为浏览器代码别人能看到。网页要加强校验时,建议放在商户自己的服务器端 PHP/Node/Java 里。

Python 校验示例:
import hmac, hashlib

def check_signature(auth, signing_secret, request_nonce):
    if auth.get("nonce") != request_nonce:
        return False
    local_sig = hmac.new(
        signing_secret.encode("utf-8"),
        auth.get("payload", "").encode("utf-8"),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(local_sig, auth.get("signature", ""))

网页 JavaScript 示例

<script>
async function getDeviceId() {
  let id = localStorage.getItem("license_device_id");
  if (!id) {
    id = "WEB-" + crypto.randomUUID();
    localStorage.setItem("license_device_id", id);
  }
  return id;
}

async function verifyCard(cardCode) {
  const res = await fetch("https://你的域名/card_auth_api.php", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      action: "verify",
      code: cardCode,
      app: "default_app",
      device: await getDeviceId(),
      scene: "web"
    })
  }).then(r => r.json());

  if (res.code === 0) {
    console.log("授权通过", res.data.card);
    return true;
  }

  alert(res.message || "授权失败");
  return false;
}

async function heartbeatCard(cardCode) {
  const res = await fetch("https://你的域名/card_auth_api.php", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      action: "heartbeat",
      code: cardCode,
      app: "default_app",
      device: await getDeviceId(),
      scene: "web"
    })
  }).then(r => r.json());

  return res.code === 0;
}
</script>

PHP 网页示例

<?php
$payload = [
  'action' => 'verify',
  'code' => $_POST['card_code'] ?? '',
  'app' => 'default_app',
  'device' => $_COOKIE['device_id'] ?? session_id(),
  'scene' => 'web'
];

$ch = curl_init('https://你的域名/card_auth_api.php');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 10
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if (($result['code'] ?? 1) === 0) {
  echo '授权通过,到期:' . $result['data']['card']['expires_text'];
} else {
  echo '授权失败:' . ($result['message'] ?? '未知错误');
}
?>

Python 脚本示例

import hashlib
import platform
import requests

def device_id():
    raw = platform.node() + "|" + platform.platform()
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()

res = requests.post(
    "https://你的域名/card_auth_api.php",
    json={
        "action": "verify",
        "code": input("请输入卡密: ").strip(),
        "app": "default_app",
        "device": device_id(),
        "scene": "script",
    },
    timeout=10,
).json()

if res.get("code") == 0:
    print("授权通过", res["data"]["card"]["expires_text"])
else:
    print("授权失败", res.get("message"))

heartbeat = requests.post(
    "https://你的域名/card_auth_api.php",
    json={
        "action": "heartbeat",
        "code": "已经验证通过的卡密",
        "app": "default_app",
        "device": device_id(),
        "scene": "script",
    },
    timeout=10,
).json()

Windows exe / C# 示例

适合 WinForms、WPF、控制台程序、.NET 打包出来的 .exe。正式项目建议把机器 GUID、硬盘序列、本地保存的随机 GUID 等组合后再 SHA256。

using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class LicenseClient
{
    static readonly string ApiUrl = "https://你的域名/card_auth_api.php";
    static readonly string AppId = "default_app";

    static string DeviceId()
    {
        var raw = Environment.MachineName + "|" + Environment.UserName;
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
        return Convert.ToHexString(bytes).ToLower();
    }

    public static async Task<bool> VerifyAsync(string cardCode)
    {
        using var client = new HttpClient();
        var body = new {
            action = "verify",
            code = cardCode,
            app = AppId,
            device = DeviceId(),
            scene = "exe"
        };

        var json = JsonSerializer.Serialize(body);
        var res = await client.PostAsync(ApiUrl, new StringContent(json, Encoding.UTF8, "application/json"));
        var text = await res.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(text);

        if (doc.RootElement.GetProperty("code").GetInt32() == 0)
        {
            Console.WriteLine("授权通过");
            return true;
        }

        Console.WriteLine("授权失败:" + doc.RootElement.GetProperty("message").GetString());
        return false;
    }

    public static async Task<bool> HeartbeatAsync(string cardCode)
    {
        using var client = new HttpClient();
        var body = new {
            action = "heartbeat",
            code = cardCode,
            app = AppId,
            device = DeviceId(),
            scene = "exe"
        };

        var json = JsonSerializer.Serialize(body);
        var res = await client.PostAsync(ApiUrl, new StringContent(json, Encoding.UTF8, "application/json"));
        var text = await res.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(text);
        return doc.RootElement.GetProperty("code").GetInt32() == 0;
    }
}

iOS ipa / Swift 示例

适合 iPhone/iPad App,打包成 .ipa 前把这段逻辑放进 App 源码里。设备码可以用 identifierForVendor,更严谨的项目可以保存一个钥匙串 Keychain 里的 UUID。

import Foundation
import UIKit

struct LicenseClient {
    static let apiUrl = URL(string: "https://你的域名/card_auth_api.php")!
    static let appId = "default_app"

    static func deviceId() -> String {
        return UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
    }

    static func verify(cardCode: String) async throws -> Bool {
        var request = URLRequest(url: apiUrl)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let payload: [String: Any] = [
            "action": "verify",
            "code": cardCode,
            "app": appId,
            "device": deviceId(),
            "scene": "ipa"
        ]
        request.httpBody = try JSONSerialization.data(withJSONObject: payload)

        let (data, _) = try await URLSession.shared.data(for: request)
        let obj = try JSONSerialization.jsonObject(with: data) as? [String: Any]
        return (obj?["code"] as? Int) == 0
    }

    static func heartbeat(cardCode: String) async throws -> Bool {
        var request = URLRequest(url: apiUrl)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let payload: [String: Any] = [
            "action": "heartbeat",
            "code": cardCode,
            "app": appId,
            "device": deviceId(),
            "scene": "ipa"
        ]
        request.httpBody = try JSONSerialization.data(withJSONObject: payload)

        let (data, _) = try await URLSession.shared.data(for: request)
        let obj = try JSONSerialization.jsonObject(with: data) as? [String: Any]
        return (obj?["code"] as? Int) == 0
    }
}

Android app / Kotlin 示例

适合 Android Studio 项目,打包成 .apk 或 .aab 前加入。设备码可以用 Settings.Secure.ANDROID_ID,并建议再做 SHA256。

import android.content.Context
import android.provider.Settings
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest

object LicenseClient {
    private const val API_URL = "https://你的域名/card_auth_api.php"
    private const val APP_ID = "default_app"

    fun deviceId(context: Context): String {
        val raw = Settings.Secure.getString(
            context.contentResolver,
            Settings.Secure.ANDROID_ID
        ) ?: "unknown"
        val bytes = MessageDigest.getInstance("SHA-256").digest(raw.toByteArray())
        return bytes.joinToString("") { "%02x".format(it.toInt() and 0xff) }
    }

    fun verify(context: Context, cardCode: String): Boolean {
        val payload = JSONObject()
            .put("action", "verify")
            .put("code", cardCode)
            .put("app", APP_ID)
            .put("device", deviceId(context))
            .put("scene", "apk")
        return postJson(payload).optInt("code") == 0
    }

    fun heartbeat(context: Context, cardCode: String): Boolean {
        val payload = JSONObject()
            .put("action", "heartbeat")
            .put("code", cardCode)
            .put("app", APP_ID)
            .put("device", deviceId(context))
            .put("scene", "apk")
        return postJson(payload).optInt("code") == 0
    }

    private fun postJson(payload: JSONObject): JSONObject {
        val conn = URL(API_URL).openConnection() as HttpURLConnection
        conn.requestMethod = "POST"
        conn.setRequestProperty("Content-Type", "application/json")
        conn.doOutput = true
        conn.outputStream.use { it.write(payload.toString().toByteArray()) }
        val text = conn.inputStream.bufferedReader().readText()
        return JSONObject(text)
    }
}

接入注意