接口地址
把下面域名换成你的网站域名:
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 只是打包后的文件格式,真正接入是在软件源码里加验证请求。所有平台流程一样:
- 软件启动或用户登录时,让用户输入卡密。
- 软件生成一个稳定设备码,调用
verify。 - 返回
code = 0才开放功能,并保存卡密和设备码。 - 软件运行中每 5 到 10 分钟调用
heartbeat。 - 心跳失败时关闭授权功能,并提示卡密到期、被禁用或设备不匹配。
软件里不要写入 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)
}
}
接入注意
app必须和你生成卡密时填写的“所属软件 app”一致。device是设备码。网页可用 localStorage 生成,软件建议用机器码、磁盘序列、系统 GUID 等组合后再做 SHA256。scene是接入格式。后台如果限制只允许 exe,客户用 ipa/apk/web 验证会失败。- 绑定设备后,同一张卡换设备会失败。需要换设备时,你在后台执行“解绑”。
- 客户永远不要调用
generate、list、manage、logs,这些都是后台管理接口。