¥
¥
快速开始
我的软件
管理您发布的所有软件
| 软件 | 状态 | 分类 | 销量 | 操作 |
|---|---|---|---|---|
|
|
|
暂无软件
基本信息
(不可修改)
根域名授权 (不可回退) 单子域名授权
编辑应用信息
修改提示
每次修改应用信息后,应用状态将自动变更为「待审核」,需要管理员重新审核后才能上架。
销售通知
当有用户购买此应用时,通过 Bark 推送通知到您的手机
启用 Bark 通知
已开启,新订单支付成功时会推送通知 未开启,开启后每次有新订单支付成功时会推送通知
Bark 是一款 iOS 推送通知应用,请在手机上安装 Bark 后获取推送地址
授权套餐
管理应用的授权价格方案
暂无授权套餐
SDK 下载
下载适合您开发语言的授权验证 SDK
PHP SDK
适用于 PHP 7.4+
Python SDK
适用于 Python 3.6+
Java SDK
适用于 Java 8+
Node.js SDK
适用于 Node.js 14+
Go SDK
适用于 Go 1.16+
C SDK
适用于 C99+
SDK 使用说明
- 点击语言卡片可查看集成代码,点击下载按钮可下载SDK文件
- 下载的 SDK 已包含您应用的公钥,可直接使用
- SDK 支持在线验证和离线验证两种模式
- 详细集成文档请查看
复制代码到您的项目中使用
上传参数备案
配置验证请求中允许的自定义上传参数
暂无上传参数备案
点击"添加参数"按钮添加允许的上传参数
拒绝原因:
上传参数说明
- 参数名只能包含字母、数字和下划线,且必须以字母或下划线开头
- 参数名长度不能超过18个字符
- 参数名不能使用系统保留关键字
- 只有备案的参数才能在验证请求中使用
- 必填参数在验证时必须提供,否则验证将失败
- 新添加的参数需要管理员审核通过后才能生效
- 审核状态:待审核、已通过、已拒绝
自定义回调参数
配置授权验证成功后返回的自定义数据
暂无回调参数
点击"添加参数"按钮添加自定义参数
回调参数说明
- 参数名只能包含字母、数字和下划线,且必须以字母或下划线开头
- 参数名和参数值都不能为空
- 这些参数将在授权验证成功后返回给客户端
密钥管理
管理应用的 RSA 密钥对,用于离线授权验证
公钥用于客户端验证离线授权文件的签名,请将此公钥嵌入到您的应用中
密钥生成时间
重新生成密钥
此操作不可撤销
警告:重新生成 RSA 密钥后,所有已下载的离线授权文件将失效,用户需要重新获取授权。请谨慎操作!
版本管理
管理应用的版本发布和更新
暂无版本记录
兑换码批次
管理应用的兑换码批次
批次 #
暂无兑换码
暂无兑换码批次
关于完整包
完整包用于上传应用的最新安装程序包,仅支持 ZIP 格式。上传后会自动覆盖之前的文件,无版本管理。
完整包管理
上传应用的安装程序包(ZIP格式,最大100MB)
或
仅支持 ZIP 格式,最大 100MB
正在上传...
关于代理商
代理商可以为您的软件创建授权,帮助您销售和管理授权。代理商只能查看自己创建的授权,无法访问请求记录等敏感信息。
代理商列表
管理此软件的代理商
| 代理商 | 联系方式 | 添加时间 | 操作 |
|---|---|---|---|
|
|
暂无代理商
代码迭代
管理应用各版本的代码文件,支持增量更新
代码列表
管理此版本的代码
此版本暂无代码文件
请选择一个版本
选择版本后可管理该版本的代码文件
为此版本添加或修改代码文件
查看文件内容
此文件标记为删除
客户端更新时将移除此文件
导入结果
ZIP文件批量导入完成
成功
失败
跳过
处理详情
资源列表
管理应用的静态资源文件,通过API获取下载地址
暂无资源
上传资源
上传静态资源文件(最大50MB)
替换资源文件
为 上传新文件
编辑资源
创建兑换码批次
为应用生成一批兑换码
授权记录
查看所有已售出的授权
| 请求ID | 应用 | 授权码 | 结果 | 请求次数 | 日期 | 最近请求 | 操作 |
|---|---|---|---|---|---|---|---|
|
绑定于
|
|
|
暂无请求记录
授权验证请求将记录在这里
¥
¥
¥
申请提现
提现记录
| 金额 | 手续费 | 实际到账 | 方式 | 状态 | 申请时间 |
|---|---|---|---|---|---|
| ¥ | ¥ | ¥ |
暂无提现记录
盗版检测记录
追踪未授权访问尝试
| 软件 | 类型 | IP地址 | 域名 | 检测原因 | 次数 | 最后检测 | 操作 |
|---|---|---|---|---|---|---|---|
|
|
暂无盗版检测记录
您的软件目前没有检测到未授权访问
开发文档
完整的集成指南和代码示例
快速开始
按照以下步骤快速集成 XingQingChuang 授权验证系统:
创建应用并获取密钥
在「软件管理」中创建您的应用,选择验证方式(域名/IP/文件/设备码),审核通过后获取公钥和私钥。
创建授权套餐
在应用详情的「授权套餐」中创建不同的套餐,设置价格、有效期和授权限制。
集成 SDK 代码
在应用详情的「检测代码」中选择您的开发语言,复制或下载 SDK 代码集成到项目中。
调用验证接口
在您的应用启动时调用 SDK 的验证方法,验证用户的授权码是否有效。
测试并上线
使用兑换码生成测试授权进行测试,确认无误后发布您的应用。
SDK 集成
选择您的开发语言,按照以下步骤集成 SDK:
1. 获取 SDK 和公钥文件
在应用详情的「检测代码」标签页中,您需要下载两个文件:
- SDK 文件:选择对应语言下载验证器代码
- 公钥文件:下载
publickey.pem文件
2. 初始化 SDK
<?php
require_once 'LicenseValidator.php';
// 创建验证器实例
$validator = new LicenseValidator([
'mode' => 'both', // 验证模式: online, offline, both
'timeout' => 10 // 请求超时时间(秒)
]);
from license_validator import LicenseValidator
# 创建验证器实例
validator = LicenseValidator(
mode='both', # 验证模式: online, offline, both
timeout=10 # 请求超时时间(秒)
)
import com.xingqingchuang.LicenseValidator;
// 创建验证器实例
LicenseValidator validator = new LicenseValidator.Builder()
.setMode("both") // 验证模式: online, offline, both
.setTimeout(10000) // 请求超时时间(毫秒)
.build();
const LicenseValidator = require('./license-validator');
// 创建验证器实例
const validator = new LicenseValidator({
mode: 'both', // 验证模式: online, offline, both
timeout: 10000 // 请求超时时间(毫秒)
});
import "xingqingchuang/license"
// 创建验证器实例
validator := license.NewValidator(license.Config{
Mode: "both", // 验证模式: online, offline, both
Timeout: 10 * time.Second, // 请求超时时间
})
#include "license_validator.h"
// 创建验证器实例
LicenseValidator* validator = license_validator_create();
license_validator_set_mode(validator, "both"); // 验证模式
license_validator_set_timeout(validator, 10); // 超时时间(秒)
3. 文件结构
将下载的 SDK 文件和公钥文件放置到您的项目中:
your-project/
├──
├── publickey.pem ← 公钥文件(必需)
└── your-app-files...
重要提示:公钥文件需单独放置
SDK 文件不再内嵌公钥内容,您需要从应用详情的「检测代码」标签页中单独下载 publickey.pem 文件,并将其放置在与 SDK 文件相同的目录下。
如果公钥文件不存在或路径不正确,SDK 将返回错误码 3010(公钥文件不存在)。
授权验证
调用验证方法检查用户的授权码是否有效,并通过回调参数实现服务端与客户端的数据交互。
验证方式说明
系统支持四种验证方式,创建应用时选择后不可更改。不同验证方式对应不同的套餐参数。
域名验证
适用于 Web 应用,绑定指定域名
套餐参数:最大域名数量
IP 验证
适用于服务器应用,绑定指定 IP
套餐参数:最大 IP 数量
文件验证
适用于离线软件,通过授权文件验证
套餐参数:最大激活次数
设备码验证
适用于桌面/移动应用,绑定设备标识
套餐参数:最大设备数量
API 端点
POST /api/v1/license/verify-encrypted
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_id | string | 是 | 应用 ID(18位字母数字) |
| encrypted_payload | string | 是 | RSA 加密的载荷(Base64 编码),包含必填参数:license_key、verify_type、verify_value,详见下方载荷结构 |
载荷结构(加密前)
以下为 RSA 加密前的 JSON 载荷参数,其中 红色背景行 为核心必填参数。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| license_key | string | 是 | 授权码,格式:XXXX-XXXX-XXXX-XXXX(16位字母数字,含3个连字符) |
| verify_type | string | 是 | 验证类型,可选值:domain | ip | device | file |
| verify_value | string | 是 | 验证值,与 verify_type 配合:域名(如 example.com)、IP地址、设备ID 或 文件哈希 |
| upload_params | object | 否 | 自定义上传参数(需预先在开发者后台备案),JSON对象格式 |
| info | string | 否 | 可选:与 upload_params 并列,不需要备案;加密的 app/.info 内容字符串(RSA 公钥加密后得到的 Base64 字符串)。传与不传不影响验证流程;无大小限制(客户端/SDK 需确保能被 RSA 分段加密承载)。注意:info 不能放在 upload_params 内;若误放入 upload_params,服务端会忽略该键。 服务端会解析并写入请求记录的「被授权用户信息」 |
| current_version | string | 否 | 当前客户端版本号(如 1.2.3),传入后响应会包含 force_update 字段 |
请求示例
{
"app_id": "YOUR_APP_ID",
"encrypted_payload": "Base64(RSA加密({
\"license_key\": \"XXXX-XXXX-XXXX-XXXX\",
\"verify_type\": \"domain\",
\"verify_value\": \"example.com\",
\"info\": \"Base64(RSA加密(app/.info内容字符串))\"
}))"
}
自动绑定功能
当授权码有效但尚未绑定验证值时,系统会自动将验证值(域名/IP/设备码/文件哈希)绑定到授权码,实现「首次验证即激活」的流程。
SDK 调用示例
使用 SDK 可以简化请求流程,SDK 会自动处理加密签名等细节:
// 发送验证请求
$result = $validator->verify('XXXX-XXXX-XXXX-XXXX', [
'info' => 'Base64(RSA加密(app/.info内容字符串))'
]);
// $result 包含服务器返回的验证结果
// 详见「接受回调」部分了解返回数据结构
# 发送验证请求
result = validator.verify('XXXX-XXXX-XXXX-XXXX', {
'info': 'Base64(RSA加密(app/.info内容字符串))'
})
# result 包含服务器返回的验证结果
# 详见「接受回调」部分了解返回数据结构
// 发送验证请求
VerifyResult result = validator.verify("XXXX-XXXX-XXXX-XXXX", Map.of(
"info", "Base64(RSA加密(app/.info内容字符串))"
));
// result 包含服务器返回的验证结果
// 详见「接受回调」部分了解返回数据结构
// 发送验证请求
const result = await validator.verify('XXXX-XXXX-XXXX-XXXX', {
info: 'Base64(RSA加密(app/.info内容字符串))'
});
// result 包含服务器返回的验证结果
// 详见「接受回调」部分了解返回数据结构
// 发送验证请求
result, err := validator.Verify("XXXX-XXXX-XXXX-XXXX", map[string]interface{}{
"info": "Base64(RSA加密(app/.info内容字符串))",
})
if err != nil {
log.Fatal(err)
}
// result 包含服务器返回的验证结果
// 详见「接受回调」部分了解返回数据结构
// 发送验证请求
VerifyResult* result = license_validator_verify(validator, "XXXX-XXXX-XXXX-XXXX");
// result 包含服务器返回的验证结果
// 详见「接受回调」部分了解返回数据结构
// 使用完毕后释放内存
verify_result_free(result);
处理验证结果
验证请求完成后,根据返回的 result 对象处理验证结果:
if ($result['valid']) {
echo "授权验证成功!";
// 获取授权信息
$data = $result['data'];
echo "到期时间: " . $data['expires_at'];
echo "剩余天数: " . $data['remaining_days'];
echo "最新版本: " . $data['latest_version'];
echo "绑定渠道: " . $data['channel']; // green=新绑定, veteran=已有绑定
// 获取功能限制
$features = $result['features'];
echo "剩余可绑定域名: " . ($features['remain_domain'] ?? 0);
// 获取自定义回调参数
if (isset($result['callback_params'])) {
$params = $result['callback_params'];
$maxUsers = $params['max_users'] ?? 10;
}
} else {
echo "授权验证失败: " . $result['message'];
$code = $result['code'] ?? 0;
}
if result['valid']:
print("授权验证成功!")
# 获取授权信息
data = result['data']
print(f"到期时间: {data['expires_at']}")
print(f"剩余天数: {data['remaining_days']}")
print(f"最新版本: {data['latest_version']}")
print(f"绑定渠道: {data['channel']}") # green=新绑定, veteran=已有绑定
# 获取功能限制
features = result['features']
print(f"剩余可绑定域名: {features.get('remain_domain', 0)}")
# 获取自定义回调参数
if 'callback_params' in result:
params = result['callback_params']
max_users = params.get('max_users', 10)
else:
print(f"授权验证失败: {result['message']}")
code = result.get('code', 0)
if (result.isValid()) {
System.out.println("授权验证成功!");
// 获取授权信息
VerifyData data = result.getData();
System.out.println("到期时间: " + data.getExpiresAt());
System.out.println("剩余天数: " + data.getRemainingDays());
System.out.println("最新版本: " + data.getLatestVersion());
System.out.println("绑定渠道: " + data.getChannel()); // green=新绑定, veteran=已有绑定
// 获取功能限制
Map<String, Object> features = result.getFeatures();
System.out.println("剩余可绑定域名: " + features.getOrDefault("remain_domain", 0));
// 获取自定义回调参数
Map<String, Object> params = result.getCallbackParams();
if (params != null) {
int maxUsers = (int) params.getOrDefault("max_users", 10);
}
} else {
System.out.println("授权验证失败: " + result.getMessage());
int code = result.getCode();
}
if (result.valid) {
console.log('授权验证成功!');
// 获取授权信息
const { data } = result;
console.log('到期时间:', data.expires_at);
console.log('剩余天数:', data.remaining_days);
console.log('最新版本:', data.latest_version);
console.log('绑定渠道:', data.channel); // green=新绑定, veteran=已有绑定
// 获取功能限制
const { features } = result;
console.log('剩余可绑定域名:', features.remain_domain || 0);
// 获取自定义回调参数
if (result.callback_params) {
const params = result.callback_params;
const maxUsers = params.max_users || 10;
}
} else {
console.log('授权验证失败:', result.message);
const code = result.code || 0;
}
if result.Valid {
fmt.Println("授权验证成功!")
// 获取授权信息
data := result.Data
fmt.Println("到期时间:", data.ExpiresAt)
fmt.Println("剩余天数:", data.RemainingDays)
fmt.Println("最新版本:", data.LatestVersion)
fmt.Println("绑定渠道:", data.Channel) // green=新绑定, veteran=已有绑定
// 获取功能限制
features := result.Features
fmt.Println("剩余可绑定域名:", features["remain_domain"])
// 获取自定义回调参数
if result.CallbackParams != nil {
params := result.CallbackParams
maxUsers := params["max_users"].(int)
}
} else {
fmt.Println("授权验证失败:", result.Message)
code := result.Code
}
if (result->valid) {
printf("授权验证成功!\n");
// 获取授权信息
printf("到期时间: %s\n", result->data->expires_at);
printf("剩余天数: %d\n", result->data->remaining_days);
printf("最新版本: %s\n", result->data->latest_version);
printf("绑定渠道: %s\n", result->data->channel); // green=新绑定, veteran=已有绑定
// 获取功能限制
printf("剩余可绑定域名: %d\n", get_feature_int(result->features, "remain_domain", 0));
// 获取自定义回调参数
if (result->callback_params) {
int max_users = get_param_int(result->callback_params, "max_users", 10);
}
} else {
printf("授权验证失败: %s\n", result->message);
int code = result->code;
}
返回数据结构
验证请求完成后,服务器会返回以下数据结构。
验证成功时返回
{
"code": 0, // 状态码 (0=成功)
"valid": true, // 是否有效
"message": "验证成功", // 消息
"features": { // 功能限制参数
"remain_domain": 3, // 剩余可绑定域名数
"remain_ip": 5, // 剩余可绑定IP数
"remain_device": 2 // 剩余可绑定设备数
},
"data": {
"channel": "green", // 绑定渠道 (green=新绑定, veteran=已有绑定)
"activated_at": "2024-01-15 10:30:00", // 激活时间
"remaining_days": 365, // 剩余天数
"expires_at": "2025-12-31 23:59:59", // 到期时间
"latest_version": "1.2.0", // 应用最新版本号
"force_update": false // 是否需要强制更新(仅当请求中包含 current_version 时返回)
},
"callback_params": { // 回调参数(服务端配置)
"api_endpoint": "https://...",
"max_users": 100
}
}
需要强制更新时返回
当 force_update 为 true 时,会额外返回 title、log 和 download_code 字段:
{
"code": 0,
"valid": true,
"message": "验证成功",
"features": { ... },
"data": {
"channel": "veteran",
"activated_at": "2024-01-15 10:30:00",
"remaining_days": 365,
"expires_at": "2025-12-31 23:59:59",
"latest_version": "2.0.0", // 最新版本号
"force_update": true, // 需要强制更新
"title": "重大更新", // 最新版本标题(仅当 force_update=true 时返回)
"log": "1. 新增功能...", // 更新日志(仅当 force_update=true 时返回)
"download_code": "ABC123XYZ" // 一次性下载码(仅当 force_update=true 时返回)
},
"callback_params": { ... }
}
参数说明
| 参数名 | 类型 | 说明 |
|---|---|---|
| code | integer | 状态码,0 表示成功 |
| valid | boolean | 验证是否通过 |
| message | string | 状态消息 |
| features | object | 功能限制参数,包含剩余可绑定数量及套餐自定义功能 |
| data.channel | string | 绑定渠道:green(新绑定)、veteran(已有绑定) |
| data.activated_at | string | 授权激活时间 |
| data.remaining_days | integer | 剩余有效天数 |
| data.expires_at | string | 授权到期时间 |
| data.latest_version | string | 应用最新版本号(如 1.0.0、1.2.0) |
| data.force_update | boolean | 是否需要强制更新(仅当请求载荷中包含 current_version 时返回) |
| data.title | string | 最新版本标题(仅当 force_update 为 true 时返回) |
| data.log | string | 最新版本更新日志(仅当 force_update 为 true 时返回) |
| data.download_code | string | 一次性下载码(9位大写字母和数字),30分钟有效(仅当 force_update 为 true 时返回) |
| callback_params | object | 服务端配置的回调参数,验证成功后返回给客户端 |
验证失败时返回
{
"code": 1001,
"valid": false,
"message": "授权码不存在",
"warning_url": "https://example.com/warning/display?token=xxx"
}
验证失败参数说明
| 参数名 | 类型 | 说明 |
|---|---|---|
| code | integer | 错误码,参见下方错误码说明 |
| valid | boolean | 验证是否通过,失败时为 false |
| message | string | 错误消息 |
| warning_url | string | 警告页面URL(可选,仅在开启盗版警告时返回) |
关于盗版警告参数
- warning_url:验证失败时返回的警告页面URL,包含一次性 token
- 仅在应用开启了盗版警告功能时返回该字段
- 客户端可将用户引导至该 URL 查看警告信息
错误码说明
在线验证错误码 (1001-1005)
| 错误码 | 错误消息 | 处理建议 |
|---|---|---|
| 0 | success | 验证成功 |
| 1001 | 授权码不存在 | 检查授权码是否正确输入 |
| 1002 | 授权已被撤销 | 联系开发者了解撤销原因 |
| 1003 | 授权已过期 | 续费或购买新授权 |
| 1004 | 域名不匹配 | 检查域名绑定是否正确 |
| 1005 | IP不匹配 | 检查IP绑定是否正确 |
离线验证错误码 (3000-3010)
| 错误码 | 错误消息 | 处理建议 |
|---|---|---|
| 3000 | 授权文件不存在 | 检查 license.json 文件是否存在 |
| 3001 | 授权文件已被篡改 | 重新下载授权文件 |
| 3002 | 验证出错 | 检查公钥文件是否正确 |
| 3003 | 授权已过期(离线) | 续费或购买新授权 |
| 3004 | 应用不匹配 | 检查授权文件是否属于当前应用 |
| 3005 | 域名不匹配(离线) | 检查域名绑定是否正确 |
| 3006 | 设备不匹配 | 检查设备绑定是否正确 |
| 3007 | 小程序AppID不匹配 | 检查小程序配置是否正确 |
| 3008 | 授权文件格式错误 | 重新下载授权文件 |
| 3009 | 网络请求失败 | 检查网络连接 |
| 3010 | 公钥文件不存在 | 将 publickey.pem 放置到正确位置 |
系统支持两种自定义参数机制:上传参数(客户端发起)和回调参数(服务端返回),实现客户端与服务器之间的双向数据传递。
| 特性 | 上传参数(发起模块) | 回调参数(回调模块) |
|---|---|---|
| 数据流向 | 客户端 → 服务器 | 服务器 → 客户端 |
| 配置方式 | 需要预先备案参数名 | 在应用设置中配置 |
| 使用场景 | 传递客户端环境信息 | 返回服务器配置数据 |
| 返回字段 | upload_params | callback_params |
1 发起模块(上传参数)
客户端在验证请求中传递自定义参数,用于向服务器报告客户端环境信息。上传参数需要预先在后台备案才能使用。
如何传递上传参数
在加密验证载荷中,将上传参数放入 upload_params 字段:
// 加密验证载荷结构
{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"verify_type": "domain",
"verify_value": "example.com",
"upload_params": {
"client_version": "1.2.0", // 客户端版本
"os_type": "windows", // 操作系统类型
"user_tag": "premium" // 用户标签
},
"info": "Base64(RSA加密(app/.info内容字符串))" // 可选:被授权用户信息
}
命名规则
- 最大长度:18 个字符
- 必须以字母或下划线开头
- 只能包含字母、数字、下划线
- 不能使用系统保留关键字
类型限制
| 类型 | 说明 | 示例 |
|---|---|---|
| string | 字符串类型,最大 255 字符 | "windows" |
| number | 数值类型,整数或浮点数 | 1024 |
| boolean | 布尔类型 | true |
备案要求
上传参数必须预先备案
- 进入「软件管理」→ 选择应用 → 「应用信息」标签
- 在「上传参数备案」区域添加参数名称
- 保存后,客户端即可使用该参数名传递数据
未备案的参数将被服务器拒绝,返回错误码 1010
保留关键字
以下关键字为系统保留,不能作为上传参数名:
license_key, app_id, app_secret, timestamp, sign, signature, verify_type, verify_value, encrypted_payload, domain, ip, device_id, file_hash, version, callback, token, key, secret, password, auth, session, cookie, header, request, response, error, code, message, data, status, id, user_id, created_at, updated_at, deleted_at
2 回调模块(回调参数)
服务器在验证成功后返回给客户端的自定义数据。回调参数在应用设置中配置,无需客户端传递。
配置方式
在应用设置中配置回调参数
- 进入「软件管理」→ 选择应用 → 「应用信息」标签
- 在「自定义回调参数」区域添加键值对
- 保存后,验证成功时会自动返回这些参数
使用场景
- 返回 API 端点地址,实现动态配置
- 返回功能开关状态,控制客户端功能
- 返回系统公告信息,推送通知给用户
- 返回资源下载地址,指引客户端获取资源
返回格式
验证成功时,回调参数会包含在响应的 callback_params 字段中:
{
"code": 0,
"valid": true,
"message": "验证成功",
"features": {
"remain_domain": 3
},
"data": {
"channel": "green",
"activated_at": "2024-01-15 10:30:00",
"remaining_days": 365,
"expires_at": "2025-12-31 23:59:59",
"latest_version": "1.2.0"
},
"callback_params": {
"api_endpoint": "https://api.example.com",
"feature_flags": {
"new_ui": true,
"beta_features": false
},
"announcement": "系统将于本周六凌晨维护",
"max_users": 100
}
}
回调参数支持嵌套对象结构,可以组织复杂的配置数据。客户端收到后可直接解析使用。
3 错误码
上传参数相关的错误码及处理建议:
| 错误码 | 错误消息 | 处理建议 |
|---|---|---|
| 1010 | 上传参数未备案 | 在应用设置中备案该参数名称后重试 |
| 1011 | 缺少必传参数 | 检查是否传递了所有标记为必传的备案参数 |
| 1000 | 参数格式错误 | 检查参数名是否符合命名规则(字母/下划线开头,仅含字母数字下划线,不超过18字符) |
当遇到上传参数错误时,验证请求会被拒绝。请确保所有上传参数都已正确备案,且参数值符合类型要求。
增值品项
增值品API允许第三方应用查询用户可用的增值品列表、生成支付二维码,并在支付完成后自动为用户授权增值品。
功能概述
查询增值品
查询用户可用的增值品列表,包含购买状态和过期时间
生成支付码
为增值品购买生成支付二维码,支持支付宝、微信、QQ支付
自动授权
支付成功后系统自动处理回调并授权增值品
使用流程
创建增值品
在「增值品管理」中创建增值品,设置名称、描述、价格和图标
绑定到应用
将增值品绑定到您的应用,只有绑定的增值品才能被查询和购买
集成API
在您的应用中集成增值品API,调用查询和支付接口
用户购买
用户扫码支付后,系统自动授权,应用可通过查询接口检查购买状态
API接口
/api/vap/query
查询增值品列表
查询用户可用的增值品列表,包含购买状态和过期时间。使用RSA加密确保请求安全性。
请求参数
加密说明:载荷包含 license_key、verify_type、verify_value、timestamp,使用应用公钥进行RSA PKCS#1 v1.5加密后Base64编码。
/api/vap/payment
生成支付二维码
为用户生成增值品购买的支付二维码。使用RSA加密确保请求安全性。
请求参数
注意:载荷包含 license_key、verify_type、verify_value、vap_id、payment_type、timestamp。二维码有效期15-30分钟,支付成功后系统自动授权。
安全机制
RSA加密
所有API请求使用RSA PKCS#1 v1.5加密,确保数据传输安全
时间戳验证
时间戳与服务器时间差不能超过5分钟,防止重放攻击
频率限制
每个应用限制60次/分钟,防止恶意刷接口
缓存优化
查询结果缓存5-10分钟,减少数据库负载
最佳实践
- 使用HTTPS协议调用API,确保数据传输安全
- 妥善保管应用私钥,不要在客户端代码中暴露
- 支付成功后等待1-2秒再查询,给系统处理回调的时间
- 实现指数退避重试策略,避免频繁请求导致限流
- 本地缓存增值品列表,减少不必要的API调用
版本管理
通过版本管理功能,您可以发布软件更新、管理代码文件,让用户获取最新版本。
创建新版本
在「软件管理」→ 应用详情 → 「版本管理」标签页中,点击「新建版本」按钮创建版本。
| 字段 | 说明 | 示例 |
|---|---|---|
| version | 版本号 | 1.2.0 |
| log | 更新日志 | 修复已知问题,新增功能... |
| force_update | 是否强制更新 | true / false |
管理代码文件
在「代码迭代」标签页中,为每个版本管理代码文件。支持三种操作类型:
新增 (add)
添加新文件到版本
更新 (update)
更新已有文件内容
删除 (delete)
标记文件为删除
代码文件支持增量更新。客户端检测更新时,系统会自动合并从当前版本到最新版本的所有变更。
发布版本
完成代码文件配置后,点击「发布」按钮将版本设为当前版本。发布后:
- 该版本成为应用的最新版本
- 客户端验证时会收到新版本号
- 客户端可通过更新检测 API 获取更新信息
客户端检测更新
客户端可通过以下 API 检测并获取更新:
检测更新 API
POST /api/v1/license/check-update
请求参数:
{
"app_id": "YOUR_APP_ID",
"encrypted_payload": "Base64编码的RSA加密数据..."
}
载荷结构(加密前):
{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"verify_type": "domain",
"verify_value": "example.com",
"current_version": "1.0.0" // 必填:当前客户端版本号
}
响应示例(有更新):
{
"valid": true,
"data": {
"updated": true,
"latest_version": "1.2.0",
"force_update": false,
"log": "1. 修复已知问题\n2. 新增功能...",
"download_code": "ABC123XYZ"
}
}
响应示例(无更新):
{
"valid": true,
"data": {
"updated": false
}
}
说明:download_code 仅在有更新时返回,用于获取代码更新 API。
获取代码更新 API
POST /api/v1/app/code-updates
请求参数:
{
"app_id": "YOUR_APP_ID",
"encrypted_payload": "Base64编码的RSA加密数据..."
}
载荷结构(加密前):
{
"current_version": "1.0.0", // 可选:当前版本号(用于计算增量更新)
"license_key": "XXXX-XXXX-XXXX-XXXX",
"verify_type": "domain",
"verify_value": "example.com",
"download_code": "ABC123XYZ"
}
字段说明:
- current_version: 当前客户端版本号,用于计算增量更新文件列表
如果不传,系统会使用检查更新时传入的版本号
- download_code: 检测更新返回的一次性下载码,必填,使用后失效
响应示例:
{
"valid": true,
"code": 0,
"data": {
"latest_version": "1.2.0",
"files": [
{
"path": "src/main.js",
"action": "update",
"url": "https://.../api/v1/license/download-file?token=XYZ789ABC",
"hash": "abc123..."
},
{
"path": "src/old-file.js",
"action": "delete"
},
{
"path": "src/new-feature.js",
"action": "add",
"url": "https://.../api/v1/license/download-file?token=DEF456GHI",
"hash": "def456..."
}
],
"codes": { // 可选:版本指令(仅当配置时返回)
"type": "sql",
"code": "ALTER TABLE users ADD COLUMN avatar VARCHAR(255);"
}
}
}
说明:
- files 中的 url 为一次性临时下载链接,30分钟内有效
- delete 操作不返回 url 和 hash
- action 类型:add(新增)、update(更新)、delete(删除)
- codes 字段仅当版本配置了指令时返回,客户端应在更新文件后执行
下载代码文件 API
GET /api/v1/license/download-file?token=XYZ789ABC
参数说明:
- token: 文件下载令牌(9位大写字母数字),从获取代码更新 API 的 url 中获取
响应:
- 成功:直接返回文件内容(二进制流)
- Content-Type 根据文件类型自动设置
使用建议:
1. 下载文件后,使用 SHA256 计算文件哈希
2. 与 API 返回的 hash 值对比,验证文件完整性
3. 验证通过后再写入本地文件
注意事项:
- token 只能使用一次,使用后立即失效
- token 有效期为 30 分钟
- 建议按顺序下载文件,避免并发请求过多
版本状态
| 状态 | 说明 |
|---|---|
| 草稿 | 版本创建后的初始状态,可编辑代码文件 |
| 已发布 | 当前生效的版本,客户端可获取此版本 |
| 历史版本 | 曾经发布过但已被新版本替代 |
增量更新机制
智能合并更新
当客户端从 v1.0 更新到 v1.2 时,系统会自动合并 v1.0 → v1.1 → v1.2 的所有变更,生成最终的文件列表。合并规则:
- 同一文件的多次更新,只保留最新版本的内容
- 先添加后删除的文件,最终结果为移除(不返回)
- 先添加后更新的文件,最终结果为添加(对用户来说是新文件)
- 删除后又添加的文件,最终结果为更新(文件重新出现)
合并规则详解
| 前一操作 | 当前操作 | 最终结果 | 说明 |
|---|---|---|---|
| add | update | add | 对用户来说是新文件 |
| add | delete | 移除 | 文件不存在于用户端 |
| update | update | update | 保留最新内容 |
| update | delete | delete | 文件需要删除 |
| delete | add | update | 文件重新出现 |
版本指令
版本指令用于在更新时执行额外操作,如数据库迁移或远程回调。在「代码迭代」标签页中,选择一个版本后可以添加或编辑该版本的指令。
| 指令类型 | 说明 | 示例 |
|---|---|---|
| SQL | 数据库迁移语句 | ALTER TABLE users ADD COLUMN age INT; |
| URL | 远程回调地址 | https://example.com/callback |
| 混合 | 同时包含 SQL 和 URL | 系统自动检测 |
安全提示
- SQL 指令禁止使用 DROP、TRUNCATE、DELETE(无 WHERE)等危险操作
- URL 回调必须使用 HTTPS 协议
- 指令在客户端更新时执行一次,请确保幂等性
版本指令 API
// 获取版本指令
GET /api/vendor/versions/{id}/command
响应示例:
{
"success": true,
"data": {
"version_id": 2,
"version": "1.1.0",
"has_command": true,
"command": "ALTER TABLE users ADD COLUMN avatar VARCHAR(255);"
}
}
// 保存版本指令(仅支持 SQL)
POST /api/vendor/versions/{id}/command
Content-Type: application/json
{
"command_code": "ALTER TABLE users ADD COLUMN avatar VARCHAR(255);"
}
// 删除版本指令
DELETE /api/vendor/versions/{id}/command
最佳实践
使用递增版本号
遵循 major.minor.patch 格式,如 1.0.0、1.1.0、2.0.0
编写清晰的更新日志
让用户了解每个版本的变更内容
谨慎使用强制更新
仅在修复严重安全漏洞或重大兼容性问题时启用
发布前充分测试
版本发布后无法撤回,请确保代码文件正确无误
版本指令保持幂等
确保指令可以安全地重复执行,避免数据不一致
全球转发
全球转发功能允许开发者上传静态资源文件,并通过 API 获取下载地址供授权用户下载。
一、功能说明
资源托管
上传配置文件、数据文件等静态资源,系统会生成唯一的资源ID用于API调用。
动态下载地址
通过 API 获取临时下载地址(非直链),有效期内可下载资源文件。
全球加速
资源文件通过 CDN 分发,用户可从最近的节点下载,提升下载速度。
二、文件限制
三、API 接口
下载资源
GET /api/v1/resources/{resource_id}?app_id={app_id}&verify_type={type}&verify_value={value}&license_key={key}
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_id | string | 是 | 资源ID(路径参数) |
| app_id | string | 是 | 应用 ID |
| verify_type | string | 是 | 验证类型: domain/ip/device/file |
| verify_value | string | 是 | 验证值(与 verify_type 对应) |
| license_key | string | 是 | 授权码 |
成功响应
直接返回文件内容(二进制流),响应头包含文件信息。
PHP 示例
// 构建下载URL
$apiUrl = "https://auth.example.com/api/v1/resources/{$resourceId}";
$apiUrl .= "?app_secret=" . urlencode($appSecret);
$apiUrl .= "&verify_type=" . urlencode($verifyType);
$apiUrl .= "&verify_value=" . urlencode($verifyValue);
$apiUrl .= "&license_key=" . urlencode($licenseKey);
// 下载文件
$content = file_get_contents($apiUrl);
file_put_contents('config.json', $content);
四、错误码
| 错误码 | 说明 |
|---|---|
| 5001 | 文件大小超过限制 |
| 5002 | 不支持的文件类型 |
| 5003 | 存储空间不足 |
| 5004 | 资源不存在 |
五、使用步骤
上传资源
在应用详情的「全球转发」标签页上传资源文件。
获取资源ID
上传成功后,复制资源ID用于API调用。
调用API获取下载地址
在软件中调用API获取临时下载地址。
下载资源
使用返回的下载地址下载资源文件。
注意事项
- 下载地址有时效性,过期后需重新获取
- 资源文件更新后,资源ID保持不变
- 建议在软件中缓存下载的资源文件
错误处理
通用错误码 (1000-1999)
| 错误码 | 说明 | 处理建议 |
|---|---|---|
| 1000 | 参数错误 | 检查请求参数 |
| 1001 | 授权码不存在 | 检查授权码 |
| 1002 | 授权已被撤销 | 联系客服 |
| 1003 | 授权已过期 | 提示续费 |
| 1009 | 验证类型不匹配 | 检查验证类型 |
| 1010 | 未备案的上传参数 | 先备案参数 |
绑定验证错误码 (3000-3999)
| 3005 | 域名不匹配 |
| 3006 | 设备不匹配 |
| 3007 | IP/小程序AppID不匹配 |
全球转发错误码 (5000-5999)
| 5001 | 文件大小超过限制(50MB) |
| 5002 | 不支持的文件类型 |
| 5003 | 存储空间不足(500MB) |
| 5004 | 资源不存在 |
API 参考
完整的 API 接口文档,包含所有授权验证相关的接口信息。
/api/v1/license/verify-encrypted
验证授权码
使用 RSA 加密的载荷验证授权码,返回授权状态和详细信息。
请求参数
载荷结构(加密前)
{
"license_key": "XXXX-XXXX-XXXX-XXXX", // 必填
"verify_type": "domain", // 必填: domain/ip/device/file
"verify_value": "example.com", // 必填: 与 verify_type 对应
"current_version": "1.0.0", // 可选: 传入后返回 force_update 字段
"upload_params": {} // 可选: 自定义上传参数
}
成功响应
{
"valid": true,
"code": 0,
"message": "验证成功",
"data": {
"channel": "veteran", // green=新绑定, veteran=已有绑定
"activated_at": "2024-01-01 00:00:00",
"remaining_days": 365, // 永久授权返回 999999
"expires_at": "2025-01-01 00:00:00",
"latest_version": "1.2.0",
"force_update": false // 仅当传入 current_version 时返回
},
"callback_params": {} // 开发者配置的回调参数
}
说明:当 force_update 为 true 时,会额外返回 title、log 和 download_code 字段。
/api/v1/app/public-key
获取应用公钥
获取应用的 RSA 公钥,用于加密验证载荷和离线验证授权签名。
请求参数
成功响应
{
"valid": true,
"code": 0,
"data": {
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----"
}
}
公钥用途:加密验证载荷(RSA PKCS#1 v1.5)、验证授权文件签名(RSA-SHA256)
/api/vap/query
查询增值品列表
查询用户可用的增值品列表,包含购买状态和过期时间。使用RSA加密确保数据安全。
请求参数
载荷结构(加密前)
{
"license_key": "XXXX-XXXX-XXXX-XXXX", // 必填
"verify_type": "domain", // 必填: domain/ip
"verify_value": "example.com", // 必填
"timestamp": 1234567890 // 必填: Unix时间戳
}
成功响应
{
"success": true,
"data": {
"products": [
{
"id": 1,
"product": "BAtUXHuHCx",
"name": "高级功能包",
"description": "包含所有高级功能",
"category": "功能扩展",
"version": "1.0.0",
"price": 99.00,
"icon": "https://example.com/icon.png",
"owned": true, // 是否已购买
"expire_time": "2024-12-31 23:59:59" // NULL表示永久
}
]
}
}
说明:时间戳与服务器时间差不能超过5分钟。查询结果会缓存5-10分钟。
/api/vap/payment
生成支付二维码
为用户生成增值品购买的支付二维码。使用RSA加密确保请求安全性。
请求参数
载荷结构(加密前)
{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"verify_type": "domain",
"verify_value": "example.com",
"vap_id": 1,
"payment_type": "alipay",
"timestamp": 1704067200
}
成功响应
{
"success": true,
"data": {
"order_id": "ORD202401010001",
"qrcode": "data:image/png;base64,iVBORw0KGgo...",
"qrcode_url": "https://qr.alipay.com/xxx",
"amount": 99.00,
"expire_time": "2024-01-01 12:30:00"
}
}
注意:二维码有效期15-30分钟。支付成功后系统自动授权,可通过轮询查询接口检查购买状态。
增值品API错误码
| 错误码 | HTTP | 说明 |
|---|---|---|
| MISSING_PARAMETER | 400 | 缺少必需参数 |
| INVALID_SIGNATURE | 401 | 签名验证失败 |
| INVALID_TIMESTAMP | 403 | 时间戳无效(超过5分钟) |
| VAP_NOT_FOUND | 404 | 增值品不存在或未上架 |
| VAP_NOT_BOUND | 403 | 增值品未绑定到应用 |
| RATE_LIMIT_EXCEEDED | 429 | 请求过于频繁(60次/分钟) |
| PAYMENT_GATEWAY_ERROR | 502 | 支付网关调用失败 |
频率限制:每个应用限制60次/分钟。建议实现指数退避重试策略。
/api/v1/resources/{resource_id}
下载资源
下载全球转发资源文件,需要授权验证。
路径参数
查询参数
响应
成功时直接返回文件内容(二进制流),Content-Type 根据文件类型自动设置。
/api/v1/license/check-update
检查更新
检查应用是否有新版本可用,返回更新信息和一次性下载码。
请求参数
载荷结构(加密前)
{
"license_key": "XXXX-XXXX-XXXX-XXXX", // 必填
"verify_type": "domain", // 必填
"verify_value": "example.com", // 必填
"current_version": "1.0.0" // 必填: 当前客户端版本号
}
成功响应(有更新)
{
"valid": true,
"code": 0,
"data": {
"updated": true,
"latest_version": "1.2.0",
"title": "版本标题",
"log": "更新日志...",
"force_update": false,
"download_code": "ABC123XYZ" // 一次性下载码,30分钟有效
}
}
注意:download_code 用于调用获取代码更新 API,使用后失效。current_version 会被缓存,用于计算增量更新。
/api/v1/app/code-updates
获取代码更新
使用下载码获取增量更新的文件列表,包含文件路径、操作类型和下载链接。
请求参数
载荷结构(加密前)
{
"current_version": "1.0.0", // 可选: 当前版本号(用于计算增量)
"license_key": "XXXX-XXXX-XXXX-XXXX", // 必填
"verify_type": "domain", // 必填
"verify_value": "example.com", // 必填
"download_code": "ABC123XYZ" // 必填: 检查更新返回的下载码
}
成功响应
{
"valid": true,
"code": 0,
"data": {
"latest_version": "1.2.0",
"files": [
{
"path": "src/main.js",
"action": "update",
"url": "https://.../download-file?token=...",
"hash": "sha256..."
},
{
"path": "src/old.js",
"action": "delete"
},
{
"path": "src/new.js",
"action": "add",
"url": "https://.../download-file?token=...",
"hash": "sha256..."
}
]
}
}
文件操作类型
说明:current_version 可选,如果不传则使用检查更新时缓存的版本号。url 为一次性临时链接,30分钟内有效。
/api/v1/license/download-file
下载代码文件
使用一次性 token 下载单个代码文件。
查询参数
响应
直接返回文件内容(二进制流),Content-Type 根据文件类型自动设置。
注意:token 只能使用一次,30分钟内有效。下载后请使用 hash 值验证文件完整性。
RSA 加密技术规范
系统使用 PKCS#1 v1.5 填充方式进行加密验证,这是 PHP 原生 OpenSSL 支持的标准加密方式,无需安装额外依赖。
加密流程概述
- 获取公钥:从
/api/v1/app/public-key接口获取应用公钥,或使用本地 publickey.pem 文件 - 构建载荷:创建包含 license_key、verify_type、verify_value、timestamp 的 JSON 对象
- 序列化:将载荷对象序列化为 UTF-8 编码的 JSON 字符串
- 检查长度:判断 JSON 字符串字节长度是否超过 245 字节
- 加密处理:≤245 字节直接加密;>245 字节需分段加密
- Base64 编码:将加密后的二进制数据转为 Base64 字符串
- 拼接结果:分段加密时用
|分隔符连接各段
加密参数规范
| 参数 | 值 | 说明 |
|---|---|---|
| 填充方式 | PKCS#1 v1.5 | PHP 原生 OpenSSL 支持 |
| 密钥长度 | 2048 位 | RSA 密钥长度 |
| 密钥字节数 | 256 字节 | 2048 / 8 = 256 |
| PKCS#1 v1.5 开销 | 11 字节 | 固定填充开销 |
| 最大明文长度 | 245 字节 | 256 - 11 = 245 |
分段加密详细说明
当验证载荷 JSON 字符串的 UTF-8 字节长度 超过 245 字节时,必须进行分段加密:
分段加密步骤:
- 将 JSON 字符串转换为 UTF-8 字节数组
- 按 245 字节为单位切分字节数组(最后一段可能不足 245 字节)
- 对每一段分别使用 RSA PKCS#1 v1.5 加密
- 将每段加密结果分别进行 Base64 编码
- 用管道符
|连接所有 Base64 字符串
示例:
假设 JSON 字符串为 600 字节:
- 第 1 段:字节 0-244(245 字节)→ 加密 → Base64 →
chunk1_base64 - 第 2 段:字节 245-489(245 字节)→ 加密 → Base64 →
chunk2_base64 - 第 3 段:字节 490-599(110 字节)→ 加密 → Base64 →
chunk3_base64 - 最终结果:
chunk1_base64|chunk2_base64|chunk3_base64
各语言完整加密示例
使用 PKCS#1 v1.5 填充,无需额外依赖
✅ 无需额外依赖
使用 PHP 原生 OpenSSL 扩展,无需安装任何第三方库。
📋 实现步骤
- 使用
file_get_contents()读取 PEM 格式公钥 - 使用
json_encode()将载荷数组序列化为 JSON 字符串 - 使用
strlen()检查字节长度是否超过 245 - 使用
openssl_public_encrypt()配合OPENSSL_PKCS1_PADDING加密 - 使用
base64_encode()编码加密结果 - 分段时使用
substr()切分,implode('|', ...)拼接
<?php
/**
* RSA PKCS#1 v1.5 加密验证载荷
*
* 使用 PHP 原生 OpenSSL 扩展,无需额外依赖
*/
/**
* RSA 加密验证载荷(支持分段加密)
*
* @param array $payload 验证载荷数组
* @param string $publicKeyPem PEM 格式公钥字符串
* @return string Base64 编码的加密数据(分段时用 | 分隔)
* @throws RuntimeException 加密失败时抛出异常
*/
function encryptPayload(array $payload, string $publicKeyPem): string
{
// 步骤 1: 将载荷序列化为 JSON 字符串(保留 Unicode 字符)
$jsonData = json_encode($payload, JSON_UNESCAPED_UNICODE);
if ($jsonData === false) {
throw new RuntimeException('JSON 序列化失败: ' . json_last_error_msg());
}
// 步骤 2: 定义单次加密最大明文长度
// 2048位密钥 = 256字节,PKCS#1 v1.5 开销 = 11字节,最大明文 = 245字节
$maxLength = 245;
$dataLength = strlen($jsonData);
// 步骤 3: 检查是否需要分段加密
if ($dataLength <= $maxLength) {
// 情况 A: 数据长度 ≤ 245 字节,单次加密
$encrypted = '';
$result = openssl_public_encrypt($jsonData, $encrypted, $publicKeyPem, OPENSSL_PKCS1_PADDING);
if ($result === false) {
throw new RuntimeException('加密失败: ' . openssl_error_string());
}
return base64_encode($encrypted);
}
// 情况 B: 数据长度 > 245 字节,需要分段加密
$encryptedChunks = [];
$offset = 0;
// 步骤 4: 按 245 字节切分并逐段加密
while ($offset < $dataLength) {
// 获取当前段(最后一段可能不足 245 字节)
$chunk = substr($jsonData, $offset, $maxLength);
// 加密当前段
$encrypted = '';
$result = openssl_public_encrypt($chunk, $encrypted, $publicKeyPem, OPENSSL_PKCS1_PADDING);
if ($result === false) {
throw new RuntimeException('分段加密失败: ' . openssl_error_string());
}
// Base64 编码并添加到数组
$encryptedChunks[] = base64_encode($encrypted);
$offset += $maxLength;
}
// 步骤 5: 用管道符连接所有加密段
return implode('|', $encryptedChunks);
}
// ==================== 完整使用示例 ====================
// 1. 读取公钥文件
$publicKeyPath = __DIR__ . '/publickey.pem';
if (!file_exists($publicKeyPath)) {
die("错误:公钥文件不存在,请从开发者中心下载 publickey.pem");
}
$publicKey = file_get_contents($publicKeyPath);
// 2. 构建验证载荷
$payload = [
'license_key' => 'XXXX-XXXX-XXXX-XXXX', // 必填:授权码
'verify_type' => 'domain', // 可选:验证类型 domain/ip/file/device
'verify_value' => 'example.com', // 可选:验证值(与 verify_type 对应)
'timestamp' => time(), // 可选:当前时间戳(秒)
'upload_params' => [ // 可选:自定义上传参数
'client_version' => '1.0.0',
'os_type' => 'linux'
]
];
// 3. 加密载荷
try {
$encryptedPayload = encryptPayload($payload, $publicKey);
echo "加密成功!\n";
echo "加密结果长度: " . strlen($encryptedPayload) . " 字符\n";
echo "是否分段: " . (strpos($encryptedPayload, '|') !== false ? '是' : '否') . "\n";
} catch (RuntimeException $e) {
die("加密失败: " . $e->getMessage());
}
// 4. 发送验证请求
$appId = 'YOUR_APP_ID'; // 替换为您的应用 ID
$apiUrl = 'https://software.xingqingchuang.com/api/v1/license/verify-encrypted';
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'app_id' => $appId,
'encrypted_payload' => $encryptedPayload
]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 5. 处理响应
$result = json_decode($response, true);
if ($result['valid']) {
echo "授权验证成功!\n";
echo "到期时间: " . $result['expires_at'] . "\n";
} else {
echo "验证失败: " . $result['message'] . " (错误码: " . $result['code'] . ")\n";
}
📦 依赖安装
cryptography 库提供了完整的 RSA PKCS#1 v1.5 支持
📋 实现步骤
- 使用
serialization.load_pem_public_key()加载 PEM 公钥 - 使用
json.dumps()序列化载荷,encode('utf-8')转字节 - 使用
len()检查字节长度 - 使用
public_key.encrypt()配合padding.PKCS1v15()加密 - PKCS#1 v1.5 填充不需要任何参数,直接调用
padding.PKCS1v15()即可 - 使用
base64.b64encode()编码,'|'.join()拼接
"""
RSA PKCS#1 v1.5 加密验证载荷 - Python 完整示例
依赖: pip install cryptography requests
"""
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import base64
import json
import time
import requests
def encrypt_payload(payload: dict, public_key_pem: str) -> str:
"""
RSA PKCS#1 v1.5 加密验证载荷
Args:
payload: 验证载荷字典
public_key_pem: PEM 格式公钥字符串
Returns:
Base64 编码的加密数据(分段时用 | 分隔)
Raises:
ValueError: 加密失败时抛出
"""
# 步骤 1: 将载荷序列化为 JSON 字符串并转换为 UTF-8 字节
json_str = json.dumps(payload, ensure_ascii=False) # 保留中文等 Unicode 字符
json_data = json_str.encode('utf-8')
# 步骤 2: 定义单次加密最大明文长度
# 2048位密钥 = 256字节,PKCS#1 v1.5 开销 = 11字节,最大明文 = 245字节
max_length = 245
# 步骤 3: 加载 PEM 格式公钥
public_key = serialization.load_pem_public_key(
public_key_pem.encode('utf-8'),
backend=default_backend()
)
# 步骤 4: 定义 PKCS#1 v1.5 填充参数
# 注意:PKCS#1 v1.5 填充不需要任何参数,直接调用即可
pkcs1_padding = padding.PKCS1v15()
# 步骤 5: 检查是否需要分段加密
data_length = len(json_data)
if data_length <= max_length:
# 情况 A: 数据长度 ≤ 245 字节,单次加密
encrypted = public_key.encrypt(json_data, pkcs1_padding)
return base64.b64encode(encrypted).decode('utf-8')
# 情况 B: 数据长度 > 245 字节,需要分段加密
# 步骤 5.1: 按 245 字节切分数据
chunks = [json_data[i:i+max_length] for i in range(0, len(json_data), max_length)]
encrypted_chunks = []
# 步骤 5.2: 逐段加密
for i, chunk in enumerate(chunks):
try:
encrypted = public_key.encrypt(chunk, pkcs1_padding)
# 步骤 5.3: 每段分别 Base64 编码
encrypted_chunks.append(base64.b64encode(encrypted).decode('utf-8'))
except Exception as e:
raise ValueError(f"第 {i} 段加密失败: {str(e)}")
# 步骤 6: 用管道符连接所有加密段
return '|'.join(encrypted_chunks)
# ==================== 完整使用示例 ====================
if __name__ == '__main__':
# 1. 读取公钥文件
public_key_path = './publickey.pem'
try:
with open(public_key_path, 'r') as f:
public_key = f.read()
except FileNotFoundError:
print(f"错误:公钥文件不存在,请从开发者中心下载 publickey.pem")
exit(1)
# 2. 构建验证载荷
payload = {
'license_key': 'XXXX-XXXX-XXXX-XXXX', # 必填:授权码
'verify_type': 'domain', # 可选:验证类型
'verify_value': 'example.com', # 可选:验证值
'timestamp': int(time.time()), # 可选:当前时间戳(秒)
'upload_params': { # 可选:自定义上传参数
'client_version': '1.0.0',
'os_type': 'linux'
}
}
# 3. 加密载荷
try:
encrypted_payload = encrypt_payload(payload, public_key)
print(f"加密成功!")
print(f"加密结果长度: {len(encrypted_payload)} 字符")
print(f"是否分段: {'是' if '|' in encrypted_payload else '否'}")
except ValueError as e:
print(f"加密失败: {e}")
exit(1)
# 4. 发送验证请求
app_id = 'YOUR_APP_ID' # 替换为您的应用 ID
api_url = 'https://software.xingqingchuang.com/api/v1/license/verify-encrypted'
response = requests.post(api_url, json={
'app_id': app_id,
'encrypted_payload': encrypted_payload
}, timeout=30)
# 5. 处理响应
result = response.json()
if result.get('valid'):
print(f"授权验证成功!")
print(f"到期时间: {result.get('expires_at')}")
else:
print(f"验证失败: {result.get('message')} (错误码: {result.get('code')})")
📦 环境要求
- Node.js 12.0+ (内置 crypto 模块支持 PKCS#1 v1.5)
- 可选安装 axios 用于 HTTP 请求:
npm install axios
📋 实现步骤
- 使用
fs.readFileSync()读取 PEM 公钥文件 - 使用
JSON.stringify()序列化载荷 - 使用
Buffer.from()转换为 UTF-8 字节 - 使用
crypto.publicEncrypt()加密 - 关键参数:
padding: RSA_PKCS1_PADDING和- 使用
toString('base64')编码,join('|')拼接 - 使用
/**
* RSA PKCS#1 v1.5 加密验证载荷 - Node.js 完整示例
* 环境要求: Node.js 12.0+
*/
const crypto = require('crypto');
const fs = require('fs');
const https = require('https');
/**
* RSA PKCS#1 v1.5 加密验证载荷
*
* @param {Object} payload - 验证载荷对象
* @param {string} publicKeyPem - PEM 格式公钥字符串
* @returns {string} Base64 编码的加密数据(分段时用 | 分隔)
* @throws {Error} 加密失败时抛出错误
*/
function encryptPayload(payload, publicKeyPem) {
// 步骤 1: 将载荷序列化为 JSON 字符串
const jsonData = JSON.stringify(payload);
// 步骤 2: 转换为 UTF-8 字节 Buffer
const buffer = Buffer.from(jsonData, 'utf-8');
// 步骤 3: 定义单次加密最大明文长度
// 2048位密钥 = 256字节,PKCS#1 v1.5 开销 = 11字节,最大明文 = 245字节
const maxLength = 245;
// 步骤 4: 定义加密选项
// 关键:必须指定 PKCS#1 v1.5 填充和 SHA-256 哈希
const encryptOptions = {
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_PADDING, // 使用 PKCS#1 v1.5 填充
};
// 步骤 5: 检查是否需要分段加密
if (buffer.length <= maxLength) {
// 情况 A: 数据长度 ≤ 245 字节,单次加密
try {
const encrypted = crypto.publicEncrypt(encryptOptions, buffer);
return encrypted.toString('base64');
} catch (error) {
throw new Error(`加密失败: ${error.message}`);
}
}
// 情况 B: 数据长度 > 245 字节,需要分段加密
// 步骤 5.1: 按 245 字节切分数据
const chunks = [];
for (let i = 0; i < buffer.length; i += maxLength) {
chunks.push(buffer.slice(i, i + maxLength));
}
// 步骤 5.2: 逐段加密
const encryptedChunks = chunks.map((chunk, index) => {
try {
const encrypted = crypto.publicEncrypt(encryptOptions, chunk);
// 步骤 5.3: 每段分别 Base64 编码
return encrypted.toString('base64');
} catch (error) {
throw new Error(`第 ${index} 段加密失败: ${error.message}`);
}
});
// 步骤 6: 用管道符连接所有加密段
return encryptedChunks.join('|');
}
// ==================== 完整使用示例 ====================
// 1. 读取公钥文件
const publicKeyPath = './publickey.pem';
let publicKey;
try {
publicKey = fs.readFileSync(publicKeyPath, 'utf-8');
} catch (error) {
console.error('错误:公钥文件不存在,请从开发者中心下载 publickey.pem');
process.exit(1);
}
// 2. 构建验证载荷
const payload = {
license_key: 'XXXX-XXXX-XXXX-XXXX', // 必填:授权码
verify_type: 'domain', // 可选:验证类型
verify_value: 'example.com', // 可选:验证值
timestamp: Math.floor(Date.now() / 1000), // 可选:当前时间戳(秒)
upload_params: { // 可选:自定义上传参数
client_version: '1.0.0',
os_type: 'linux'
}
};
// 3. 加密载荷
let encryptedPayload;
try {
encryptedPayload = encryptPayload(payload, publicKey);
console.log('加密成功!');
console.log(`加密结果长度: ${encryptedPayload.length} 字符`);
console.log(`是否分段: ${encryptedPayload.includes('|') ? '是' : '否'}`);
} catch (error) {
console.error(`加密失败: ${error.message}`);
process.exit(1);
}
// 4. 发送验证请求
const appId = 'YOUR_APP_ID'; // 替换为您的应用 ID
const requestData = JSON.stringify({
app_id: appId,
encrypted_payload: encryptedPayload
});
const options = {
hostname: 'software.xingqingchuang.com',
port: 443,
path: '/api/v1/license/verify-encrypted',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestData)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
// 5. 处理响应
const result = JSON.parse(data);
if (result.valid) {
console.log('授权验证成功!');
console.log(`到期时间: ${result.expires_at}`);
} else {
console.log(`验证失败: ${result.message} (错误码: ${result.code})`);
}
});
});
req.on('error', error => console.error(`请求失败: ${error.message}`));
req.write(requestData);
req.end();
📦 环境要求
- Java 8+ (JCE 支持 PKCS#1 v1.5 填充)
- 无需额外依赖,使用标准库
javax.crypto - 可选:使用 OkHttp 或 HttpClient 发送请求
📋 实现步骤
- 读取 PEM 文件,移除头尾标记,Base64 解码得到公钥字节
- 使用
X509EncodedKeySpec和KeyFactory构建公钥对象 - 使用
Cipher.getInstance("RSA/ECB/PKCS1Padding") - 将 JSON 字符串转为 UTF-8 字节数组
- 检查长度,超过 245 字节时分段处理
- 使用
Base64.getEncoder()编码,String.join("|", ...)拼接
import javax.crypto.Cipher;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
/**
* RSA PKCS#1 v1.5 加密验证载荷 - Java 完整示例
* 环境要求: Java 8+
*/
public class RSAEncryptor {
// 2048位密钥 = 256字节,PKCS#1 v1.5 开销 = 11字节,最大明文 = 245字节
private static final int MAX_ENCRYPT_BLOCK = 245;
/**
* RSA PKCS#1 v1.5 加密验证载荷
*
* @param jsonPayload JSON 格式的验证载荷字符串
* @param publicKeyPem PEM 格式公钥字符串
* @return Base64 编码的加密数据(分段时用 | 分隔)
* @throws Exception 加密失败时抛出异常
*/
public static String encryptPayload(String jsonPayload, String publicKeyPem) throws Exception {
// 步骤 1: 解析 PEM 格式公钥
// 移除 PEM 头尾标记和换行符
String publicKeyContent = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", ""); // 移除所有空白字符
// 步骤 2: Base64 解码得到公钥字节
byte[] keyBytes = Base64.getDecoder().decode(publicKeyContent);
// 步骤 3: 构建公钥对象
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// 步骤 4: 初始化加密器
// 关键:使用 PKCS1Padding 指定 PKCS#1 v1.5 填充和 SHA-256 哈希
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 步骤 5: 将 JSON 字符串转为 UTF-8 字节数组
byte[] data = jsonPayload.getBytes(StandardCharsets.UTF_8);
// 步骤 6: 检查是否需要分段加密
if (data.length <= MAX_ENCRYPT_BLOCK) {
// 情况 A: 数据长度 ≤ 245 字节,单次加密
byte[] encrypted = cipher.doFinal(data);
return Base64.getEncoder().encodeToString(encrypted);
}
// 情况 B: 数据长度 > 245 字节,需要分段加密
List<String> encryptedChunks = new ArrayList<>();
int offset = 0;
int chunkIndex = 0;
// 步骤 6.1: 按 245 字节切分并逐段加密
while (offset < data.length) {
// 计算当前段长度(最后一段可能不足 245 字节)
int length = Math.min(MAX_ENCRYPT_BLOCK, data.length - offset);
// 复制当前段数据
byte[] chunk = new byte[length];
System.arraycopy(data, offset, chunk, 0, length);
// 加密当前段
byte[] encrypted = cipher.doFinal(chunk);
// 步骤 6.2: Base64 编码并添加到列表
encryptedChunks.add(Base64.getEncoder().encodeToString(encrypted));
offset += length;
chunkIndex++;
}
// 步骤 7: 用管道符连接所有加密段
return String.join("|", encryptedChunks);
}
/**
* 从文件读取公钥
*/
public static String loadPublicKey(String filePath) throws IOException {
return new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
}
// ==================== 完整使用示例 ====================
public static void main(String[] args) {
try {
// 1. 读取公钥文件
String publicKeyPath = "./publickey.pem";
String publicKey = loadPublicKey(publicKeyPath);
// 2. 构建验证载荷 JSON
// 注意:实际项目中建议使用 Jackson 或 Gson 库
long timestamp = System.currentTimeMillis() / 1000;
String jsonPayload = String.format(
"{\"license_key\":\"XXXX-XXXX-XXXX-XXXX\"," +
"\"verify_type\":\"domain\"," +
"\"verify_value\":\"example.com\"," +
"\"timestamp\":%d," +
"\"upload_params\":{\"client_version\":\"1.0.0\",\"os_type\":\"linux\"}}",
timestamp
);
// 3. 加密载荷
String encryptedPayload = encryptPayload(jsonPayload, publicKey);
System.out.println("加密成功!");
System.out.println("加密结果长度: " + encryptedPayload.length() + " 字符");
System.out.println("是否分段: " + (encryptedPayload.contains("|") ? "是" : "否"));
// 4. 发送验证请求(使用 HttpURLConnection 示例)
String appId = "YOUR_APP_ID"; // 替换为您的应用 ID
String apiUrl = "https://software.xingqingchuang.com/api/v1/license/verify-encrypted";
// 构建请求体
String requestBody = String.format(
"{\"app_id\":\"%s\",\"encrypted_payload\":\"%s\"}",
appId, encryptedPayload
);
// 发送 POST 请求
java.net.URL url = new java.net.URL(apiUrl);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
try (java.io.OutputStream os = conn.getOutputStream()) {
os.write(requestBody.getBytes(StandardCharsets.UTF_8));
}
// 5. 读取响应
int responseCode = conn.getResponseCode();
java.io.InputStream is = responseCode == 200 ? conn.getInputStream() : conn.getErrorStream();
String response = new String(is.readAllBytes(), StandardCharsets.UTF_8);
System.out.println("响应: " + response);
} catch (Exception e) {
System.err.println("错误: " + e.getMessage());
e.printStackTrace();
}
}
}
📦 环境要求
- Go 1.16+ (标准库 crypto/rsa 支持 PKCS#1 v1.5)
- 无需额外依赖,使用标准库
crypto/rsa、crypto/sha256 - 可选:使用
net/http发送 HTTP 请求
📋 实现步骤
- 使用
os.ReadFile()读取 PEM 公钥文件 - 使用
pem.Decode()解析 PEM 格式,获取 DER 字节 - 使用
x509.ParsePKIXPublicKey()解析公钥,类型断言为*rsa.PublicKey - 使用
json.Marshal()序列化载荷为 JSON 字节切片 - 使用
len()检查字节长度是否超过 190 - 使用
rsa.EncryptPKCS1v15(rand.Reader, publicKey, data)加密 - 使用
base64.StdEncoding.EncodeToString()编码 - 分段时使用切片操作
data[i:end],strings.Join(chunks, "|")拼接
⚠️ Go 语言特别注意
rsa.EncryptPKCS1v15()只需要 3 个参数:随机数生成器、公钥、明文数据- 第一个参数
rand.Reader是加密安全随机数生成器,不可省略 - Go 的字符串是 UTF-8 编码,
[]byte(str)直接得到 UTF-8 字节
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// 2048位密钥 = 256字节,PKCS#1 v1.5 开销 = 11字节,最大明文 = 245字节
const maxEncryptBlock = 245
// VerifyPayload 验证载荷结构
type VerifyPayload struct {
LicenseKey string `json:"license_key"` // 必填:授权码
VerifyType string `json:"verify_type,omitempty"` // 可选:验证类型
VerifyValue string `json:"verify_value,omitempty"` // 可选:验证值
Timestamp int64 `json:"timestamp,omitempty"` // 可选:时间戳
UploadParams map[string]interface{} `json:"upload_params,omitempty"`// 可选:自定义参数
}
// EncryptPayload RSA PKCS#1 v1.5 加密验证载荷
//
// 参数:
// - payload: 验证载荷结构体
// - publicKeyPem: PEM 格式公钥字符串
//
// 返回:
// - string: Base64 编码的加密数据(分段时用 | 分隔)
// - error: 加密失败时返回错误
func EncryptPayload(payload VerifyPayload, publicKeyPem string) (string, error) {
// 步骤 1: 将载荷序列化为 JSON 字节切片
jsonData, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("JSON 序列化失败: %w", err)
}
// 步骤 2: 解析 PEM 格式公钥
// pem.Decode 返回 PEM 块和剩余数据
block, _ := pem.Decode([]byte(publicKeyPem))
if block == nil {
return "", fmt.Errorf("PEM 解析失败: 无效的 PEM 格式")
}
// 步骤 3: 解析 PKIX 格式公钥(X.509 SubjectPublicKeyInfo)
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("公钥解析失败: %w", err)
}
// 步骤 4: 类型断言为 RSA 公钥
publicKey, ok := pub.(*rsa.PublicKey)
if !ok {
return "", fmt.Errorf("公钥类型错误: 期望 RSA 公钥")
}
// 步骤 5: 检查是否需要分段加密
dataLen := len(jsonData)
if dataLen <= maxEncryptBlock {
// 情况 A: 数据长度 ≤ 245 字节,单次加密
// rsa.EncryptPKCS1v15 参数说明:
// - rand.Reader: 加密安全随机数生成器
// - publicKey: RSA 公钥
// - jsonData: 待加密的明文
encrypted, err := rsa.EncryptPKCS1v15(
rand.Reader,
publicKey,
jsonData,
)
if err != nil {
return "", fmt.Errorf("加密失败: %w", err)
}
return base64.StdEncoding.EncodeToString(encrypted), nil
}
// 情况 B: 数据长度 > 245 字节,需要分段加密
var encryptedChunks []string
// 步骤 5.1: 按 245 字节切分并逐段加密
for i := 0; i < dataLen; i += maxEncryptBlock {
// 计算当前段结束位置(最后一段可能不足 245 字节)
end := i + maxEncryptBlock
if end > dataLen {
end = dataLen
}
// 获取当前段数据
chunk := jsonData[i:end]
// 加密当前段
encrypted, err := rsa.EncryptPKCS1v15(
rand.Reader,
publicKey,
chunk,
)
if err != nil {
return "", fmt.Errorf("第 %d 段加密失败: %w", i/maxEncryptBlock, err)
}
// 步骤 5.2: Base64 编码并添加到切片
encryptedChunks = append(encryptedChunks, base64.StdEncoding.EncodeToString(encrypted))
}
// 步骤 6: 用管道符连接所有加密段
return strings.Join(encryptedChunks, "|"), nil
}
// LoadPublicKey 从文件加载公钥
func LoadPublicKey(filePath string) (string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("读取公钥文件失败: %w", err)
}
return string(data), nil
}
// ==================== 完整使用示例 ====================
func main() {
// 1. 读取公钥文件
publicKeyPath := "./publickey.pem"
publicKey, err := LoadPublicKey(publicKeyPath)
if err != nil {
fmt.Printf("错误:%v\n请从开发者中心下载 publickey.pem\n", err)
os.Exit(1)
}
// 2. 构建验证载荷
payload := VerifyPayload{
LicenseKey: "XXXX-XXXX-XXXX-XXXX", // 必填:授权码
VerifyType: "domain", // 可选:验证类型
VerifyValue: "example.com", // 可选:验证值
Timestamp: time.Now().Unix(), // 可选:当前时间戳(秒)
UploadParams: map[string]interface{}{ // 可选:自定义上传参数
"client_version": "1.0.0",
"os_type": "linux",
},
}
// 3. 加密载荷
encryptedPayload, err := EncryptPayload(payload, publicKey)
if err != nil {
fmt.Printf("加密失败: %v\n", err)
os.Exit(1)
}
fmt.Println("加密成功!")
fmt.Printf("加密结果长度: %d 字符\n", len(encryptedPayload))
if strings.Contains(encryptedPayload, "|") {
fmt.Println("是否分段: 是")
} else {
fmt.Println("是否分段: 否")
}
// 4. 发送验证请求
appID := "YOUR_APP_ID" // 替换为您的应用 ID
apiURL := "https://software.xingqingchuang.com/api/v1/license/verify-encrypted"
// 构建请求体
requestBody := map[string]string{
"app_id": appID,
"encrypted_payload": encryptedPayload,
}
jsonBody, _ := json.Marshal(requestBody)
// 发送 POST 请求
resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
fmt.Printf("请求失败: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
// 5. 读取并处理响应
body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
if valid, ok := result["valid"].(bool); ok && valid {
fmt.Println("授权验证成功!")
if expiresAt, ok := result["expires_at"].(string); ok {
fmt.Printf("到期时间: %s\n", expiresAt)
}
} else {
msg := result["message"]
code := result["code"]
fmt.Printf("验证失败: %v (错误码: %v)\n", msg, code)
}
}
📦 依赖安装
Ubuntu/Debian:
macOS (Homebrew):
编译命令:
📋 实现步骤
- 使用
BIO_new_mem_buf()创建内存 BIO 读取 PEM 公钥 - 使用
PEM_read_bio_PUBKEY()解析 PEM 格式公钥 - 使用
EVP_PKEY_CTX_new()创建加密上下文 - 使用
EVP_PKEY_CTX_set_rsa_padding(RSA_PKCS1_PADDING)设置 PKCS#1 v1.5 填充 - 使用
EVP_PKEY_CTX_set_rsa_padding(EVP_sha256())设置 SHA-256 哈希 - 使用
EVP_PKEY_CTX_set_rsa_mgf1_md(EVP_sha256())设置 MGF1 哈希 - 使用
strlen()检查字节长度,超过 245 字节时分段处理 - 使用
EVP_PKEY_encrypt()执行加密 - 使用
EVP_EncodeBlock()进行 Base64 编码
⚠️ C 语言特别注意
- OpenSSL 1.1.0+ 推荐使用
EVP_PKEYAPI,旧版RSA_public_encrypt已废弃 - 必须同时设置
padding和mgf1_md为 SHA-256 - 注意内存管理:使用
free()释放分配的内存,EVP_PKEY_free()释放密钥 - Base64 编码后长度约为原始长度的 4/3,分配缓冲区时需预留空间
- 分段加密时,每段加密结果为 256 字节,Base64 后约 344 字符
/**
* RSA PKCS#1 v1.5 加密验证载荷 - C 语言完整示例
*
* 依赖: OpenSSL 1.1.0+
* 编译: gcc -o rsa_encrypt rsa_encrypt.c -lssl -lcrypto
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
// 2048位密钥 = 256字节,PKCS#1 v1.5 开销 = 11字节,最大明文 = 245字节
#define MAX_ENCRYPT_BLOCK 245
// RSA 2048 加密后的密文长度
#define RSA_CIPHERTEXT_LEN 256
/**
* Base64 编码
*
* @param input 输入数据
* @param input_len 输入长度
* @param output_len 输出长度指针
* @return Base64 编码后的字符串(需要调用者 free)
*/
char* base64_encode(const unsigned char* input, int input_len, int* output_len) {
// Base64 编码后长度 = ceil(input_len / 3) * 4 + 1
int encoded_len = ((input_len + 2) / 3) * 4 + 1;
char* output = (char*)malloc(encoded_len);
if (!output) return NULL;
*output_len = EVP_EncodeBlock((unsigned char*)output, input, input_len);
output[*output_len] = '\0';
return output;
}
/**
* 使用 RSA PKCS#1 v1.5 加密单个数据块
*
* @param pkey EVP_PKEY 公钥对象
* @param data 待加密数据
* @param data_len 数据长度
* @param encrypted_len 加密后长度指针
* @return 加密后的数据(需要调用者 free),失败返回 NULL
*/
unsigned char* encrypt_block(EVP_PKEY* pkey, const unsigned char* data,
size_t data_len, size_t* encrypted_len) {
EVP_PKEY_CTX* ctx = NULL;
unsigned char* encrypted = NULL;
// 步骤 1: 创建加密上下文
ctx = EVP_PKEY_CTX_new(pkey, NULL);
if (!ctx) {
fprintf(stderr, "创建加密上下文失败\n");
return NULL;
}
// 步骤 2: 初始化加密操作
if (EVP_PKEY_encrypt_init(ctx) <= 0) {
fprintf(stderr, "初始化加密失败\n");
EVP_PKEY_CTX_free(ctx);
return NULL;
}
// 步骤 3: 设置 PKCS#1 v1.5 填充方式
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) {
fprintf(stderr, "设置 PKCS#1 v1.5 填充失败\n");
EVP_PKEY_CTX_free(ctx);
return NULL;
}
// 步骤 4: 设置 哈希算法为 SHA-256
if (EVP_PKEY_CTX_set_rsa_padding(ctx, EVP_sha256()) <= 0) {
fprintf(stderr, "设置 哈希算法失败\n");
EVP_PKEY_CTX_free(ctx);
return NULL;
}
// 步骤 5: 设置 MGF1 哈希算法为 SHA-256
if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) {
fprintf(stderr, "设置 MGF1 哈希算法失败\n");
EVP_PKEY_CTX_free(ctx);
return NULL;
}
// 步骤 6: 获取加密后数据长度
if (EVP_PKEY_encrypt(ctx, NULL, encrypted_len, data, data_len) <= 0) {
fprintf(stderr, "获取加密长度失败\n");
EVP_PKEY_CTX_free(ctx);
return NULL;
}
// 步骤 7: 分配加密结果缓冲区
encrypted = (unsigned char*)malloc(*encrypted_len);
if (!encrypted) {
fprintf(stderr, "内存分配失败\n");
EVP_PKEY_CTX_free(ctx);
return NULL;
}
// 步骤 8: 执行加密
if (EVP_PKEY_encrypt(ctx, encrypted, encrypted_len, data, data_len) <= 0) {
fprintf(stderr, "加密失败: ");
ERR_print_errors_fp(stderr);
free(encrypted);
EVP_PKEY_CTX_free(ctx);
return NULL;
}
EVP_PKEY_CTX_free(ctx);
return encrypted;
}
/**
* RSA PKCS#1 v1.5 加密验证载荷(支持分段加密)
*
* @param json_payload JSON 格式的验证载荷字符串
* @param public_key_pem PEM 格式公钥字符串
* @return Base64 编码的加密数据(分段时用 | 分隔),失败返回 NULL
* 需要调用者 free 返回的字符串
*/
char* encrypt_payload(const char* json_payload, const char* public_key_pem) {
BIO* bio = NULL;
EVP_PKEY* pkey = NULL;
char* result = NULL;
// 步骤 1: 创建内存 BIO 读取 PEM 公钥
bio = BIO_new_mem_buf(public_key_pem, -1);
if (!bio) {
fprintf(stderr, "创建 BIO 失败\n");
return NULL;
}
// 步骤 2: 解析 PEM 格式公钥
pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
BIO_free(bio);
if (!pkey) {
fprintf(stderr, "解析公钥失败: ");
ERR_print_errors_fp(stderr);
return NULL;
}
// 步骤 3: 获取数据长度
size_t data_len = strlen(json_payload);
const unsigned char* data = (const unsigned char*)json_payload;
// 步骤 4: 检查是否需要分段加密
if (data_len <= MAX_ENCRYPT_BLOCK) {
// 情况 A: 数据长度 ≤ 245 字节,单次加密
size_t encrypted_len;
unsigned char* encrypted = encrypt_block(pkey, data, data_len, &encrypted_len);
if (!encrypted) {
EVP_PKEY_free(pkey);
return NULL;
}
// Base64 编码
int base64_len;
result = base64_encode(encrypted, encrypted_len, &base64_len);
free(encrypted);
EVP_PKEY_free(pkey);
return result;
}
// 情况 B: 数据长度 > 245 字节,需要分段加密
// 计算分段数量
size_t num_chunks = (data_len + MAX_ENCRYPT_BLOCK - 1) / MAX_ENCRYPT_BLOCK;
// 预分配结果缓冲区
// 每段 Base64 约 344 字符 + 分隔符 + 结束符
size_t result_size = num_chunks * 350 + 1;
result = (char*)malloc(result_size);
if (!result) {
fprintf(stderr, "内存分配失败\n");
EVP_PKEY_free(pkey);
return NULL;
}
result[0] = '\0';
// 步骤 5: 逐段加密
size_t offset = 0;
int chunk_index = 0;
while (offset < data_len) {
// 计算当前段长度
size_t chunk_len = data_len - offset;
if (chunk_len > MAX_ENCRYPT_BLOCK) {
chunk_len = MAX_ENCRYPT_BLOCK;
}
// 加密当前段
size_t encrypted_len;
unsigned char* encrypted = encrypt_block(pkey, data + offset, chunk_len, &encrypted_len);
if (!encrypted) {
fprintf(stderr, "第 %d 段加密失败\n", chunk_index);
free(result);
EVP_PKEY_free(pkey);
return NULL;
}
// Base64 编码
int base64_len;
char* base64_chunk = base64_encode(encrypted, encrypted_len, &base64_len);
free(encrypted);
if (!base64_chunk) {
fprintf(stderr, "第 %d 段 Base64 编码失败\n", chunk_index);
free(result);
EVP_PKEY_free(pkey);
return NULL;
}
// 拼接结果(第一段不加分隔符)
if (chunk_index > 0) {
strcat(result, "|");
}
strcat(result, base64_chunk);
free(base64_chunk);
offset += chunk_len;
chunk_index++;
}
EVP_PKEY_free(pkey);
return result;
}
/**
* 从文件读取公钥
*/
char* load_public_key(const char* file_path) {
FILE* fp = fopen(file_path, "r");
if (!fp) {
fprintf(stderr, "无法打开公钥文件: %s\n", file_path);
return NULL;
}
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
char* content = (char*)malloc(file_size + 1);
if (!content) {
fclose(fp);
return NULL;
}
fread(content, 1, file_size, fp);
content[file_size] = '\0';
fclose(fp);
return content;
}
// ==================== 完整使用示例 ====================
int main() {
// 初始化 OpenSSL
OpenSSL_add_all_algorithms();
ERR_load_crypto_strings();
// 1. 读取公钥文件
const char* public_key_path = "./publickey.pem";
char* public_key = load_public_key(public_key_path);
if (!public_key) {
fprintf(stderr, "错误:公钥文件不存在,请从开发者中心下载 publickey.pem\n");
return 1;
}
// 2. 构建验证载荷 JSON
// 注意:实际项目中建议使用 cJSON 等 JSON 库
time_t timestamp = time(NULL);
char json_payload[1024];
snprintf(json_payload, sizeof(json_payload),
"{\"license_key\":\"XXXX-XXXX-XXXX-XXXX\","
"\"verify_type\":\"domain\","
"\"verify_value\":\"example.com\","
"\"timestamp\":%ld,"
"\"upload_params\":{\"client_version\":\"1.0.0\",\"os_type\":\"linux\"}}",
timestamp
);
printf("原始载荷长度: %zu 字节\n", strlen(json_payload));
// 3. 加密载荷
char* encrypted_payload = encrypt_payload(json_payload, public_key);
free(public_key);
if (!encrypted_payload) {
fprintf(stderr, "加密失败\n");
return 1;
}
printf("加密成功!\n");
printf("加密结果长度: %zu 字符\n", strlen(encrypted_payload));
printf("是否分段: %s\n", strchr(encrypted_payload, '|') ? "是" : "否");
// 4. 打印加密结果(实际使用时发送到 API)
printf("\n加密结果:\n%s\n", encrypted_payload);
// 5. 清理
free(encrypted_payload);
// 清理 OpenSSL
EVP_cleanup();
ERR_free_strings();
printf("\n提示: 请将加密结果发送到 /api/v1/license/verify-encrypted 接口\n");
printf("请求体格式: {\"app_id\":\"YOUR_APP_ID\",\"encrypted_payload\":\"...\"}\n");
return 0;
}
注意事项
- 保持载荷简洁:建议验证载荷保持在 245 字节以内,避免分段加密的复杂性
- 时间戳可选:载荷中可包含 timestamp 字段,用于防止重放攻击(有效期 5 分钟)
- 编码一致性:JSON 序列化时使用 UTF-8 编码
- 公钥获取:可通过
/api/v1/app/public-key接口获取应用公钥
| 优惠码 | 名称 | 折扣 | 适用范围 | 使用情况 | 有效期 | 状态 | 操作 |
|---|---|---|---|---|---|---|---|
|
加载中... |
|||||||
|
暂无优惠券 |
|||||||
|
|
|
|||||
共 条记录
我的增值品
管理您发布的所有增值品
| 增值品 | 价格 | 状态 | 绑定应用 | 创建时间 | 操作 |
|---|---|---|---|---|---|
|
|
¥ | 个应用 |
|
暂无增值品
共 个增值品
自我检测功能
此功能用于测试验证流程,测试请求不会影响生产数据。您可以模拟不同验证场景,查看完整的回调数据结构。
测试历史
暂无测试记录
测试配置
选择应用并填写测试参数
填写后响应会包含 force_update 字段
注意:info 需要与 upload_params 并列(不要放在 upload_params 里)
该应用未配置上传参数
info 文件解析
上传 info 内容并使用私钥解密查看内容
从应用安装包中提取的 app/.info 内容
解析结果
解析失败
测试结果将在此处显示
填写左侧表单并点击"发送测试请求"按钮,即可查看验证结果
请求数据结构示例 (Request)
{
"app_id": "abc123def456", // 应用ID (必填)
"license_key": "XXXX-XXXX-XXXX-XXXX", // 授权码 (必填)
"verify_type": "domain", // 验证类型: domain/ip/device/file (必填)
"verify_value": "example.com", // 验证值 (必填)
"upload_params": { // 上传参数 (可选)
"machine_code": "ABC123",
"os_version": "Windows 11"
}
}
verify_type 可选值:
domain 域名验证 |
ip IP验证 |
device 设备验证 |
file 文件哈希验证
回调数据结构示例 (Response)
{
"valid": true, // 验证结果: true/false
"code": 0, // 响应码: 0=成功, 其他=错误码
"message": "验证成功", // 响应消息
"license_key": "XXXX-XXXX-XXXX-XXXX", // 授权码
"activated_at": "2024-01-15 10:30:00", // 激活时间
"expires_at": "2025-01-15 10:30:00", // 过期时间 (永久授权为 "2099-12-31 24:00:00")
"remaining_days": 365, // 剩余天数 (永久授权为 999999)
"latest_version": "2.1.0", // 应用最新版本号
"channel": "green", // 绑定渠道: "green"=新绑定, "veteran"=已有绑定, null=无绑定操作
"features": { // 套餐限制参数
"remain_domain": 2, // 剩余可绑定域名数 (域名验证时)
"remain_ip": 3, // 剩余可绑定IP数 (IP验证时)
"remain_device": 1, // 剩余可绑定设备数 (设备验证时)
"remain_file": 5 // 剩余可绑定文件数 (文件验证时)
},
"callback_params": { // 回调参数 (开发者配置)
"api_endpoint": "https://api.example.com",
"custom_key": "custom_value"
}
}
说明:
features 中的字段根据验证类型动态返回;
channel 字段始终返回,标识绑定状态。
错误码说明
0
验证成功
1001
授权码不存在
1002
授权已被撤销
1003
授权已过期
1004
域名不匹配
1005
IP不匹配
1006
设备不匹配
1007
文件哈希不匹配
1008
绑定数量已达上限
1009
应用不存在
1010
应用已下架
9999
系统错误
网络连接失败
请求失败
请求内容 (Request Content)
回调内容 (Response Content)
套餐限制参数 (features)
回调参数 (callback_params)
下载测试
使用下载码调用下载接口,测试资源下载功能