软件授权验证接口文档

本页是 得搜软件 软件网络验证服务的接入说明,任何支持 HTTP POST 的语言都能对接。 协议分三层:业务参数打成 JSON → 用安全码做 RC4 加密 → 再按 MD5 规则算签名。 读完这一页就能把你的软件接到授权系统上。

POST + RC4 + MD5 签名 明文 GBK 编码 15 个接口 按次数 / 按天 两种试用 PHP / Python / C# / 易语言 示例
本文档只讲协议格式,不包含任何密钥。你的 appid、安全码、自定义码请登录管理后台, 在「接口文档」页查看并复制 —— 那是你账号专属的签发凭据,不要贴到公开仓库或发给他人。

接入信息

接口地址
https://www.andsoo.com/UserApi
请求方式
POST,Content-Type: application/x-www-form-urlencoded; charset=GBK
软件 appid
你的 appid
登录软件后台,在「软件管理」列表里能看到每款软件的 appid(就是它的 id)。
安全码 safeCode
你的安全码 — 加密请求数据 + 参与签名
每个软件独立一份,登录后台在「接口文档」页可复制到本账号的真实值。
自定义码 customCode
你的自定义码 — 解密服务端返回的数据
时间戳容差
30 秒(超出返回 status = -3)
密钥就是你的防破防线。安全码与自定义码会编译进客户端,请勿使用默认值、也勿写入公开仓库。 后台「软件管理 → 重新生成密钥」可以一键换新,但换完必须同步更新客户端。

协议说明

1. 请求格式

POST 到接口地址,表单参数如下:

参数类型必需说明
appid整数是软件 ID,在后台软件列表查看
timestamp长整数是毫秒级时间戳,与服务端相差不得超过容差秒数
data文本是业务参数 JSON 经 RC4 加密后的 HEX(大写)
sign文本是参数签名,见下方算法
token文本否登录后拿到的会话 token;与 cookie 二选一,共存时以 token 为准

2. 加解密(RC4)

data 的明文是一个 JSON 对象,采用标准 RC4 流加密,密钥是字符串:

  • 请求方向:明文 → RC4(安全码) → HEX 大写 → 作为 data 提交
  • 响应方向:收到的 data → HEX 还原 → RC4(自定义码) → 得到明文 JSON
  • 明文 JSON 使用 GBK 编码(不是 UTF-8),这样中文提示在客户端才不乱码

3. 签名算法

sign = md5( appid + 安全码 + timestamp + data密文 ) // 32 位小写

四段直接首尾相连,中间不加任何分隔符。注意要用加密之后的 data 密文参与计算。

4. 响应格式

{ "status": 1, // 系统状态码,1 = 成功 "msg": "", // 系统提示,一般在 status 非 1 时有值 "token": "b64e2dad-5ac1-...", // 初始化/登录时返回 "data": "28D2DDEB8B10..." // 密文,用自定义码解密后是业务 JSON }

data 解密后的业务 JSON:

{ "code": 200, // 业务码,200 = 成功 "msg": "登录成功", // 业务提示,可直接显示给用户 "data": { ... }, // 各接口不同 "timestamp": 1549448450434 // 服务端毫秒时间戳 }
判断逻辑建议:先看外层 status 是否等于 1;等于 1 再解密 data, 再看内层 code 是否等于 200。两层都不通过时优先报外层(多为客户端配置问题)。

试用策略

每款软件只能选一种,时长单位统一为「天」

试用方式由管理员在后台按软件配置,两种方式互斥,只能启用其中一种。客户端不要写死逻辑, 先读 Initialize 返回的 trialMode,再按模式走对应分支。

times 按次数
  • trialTimes = 每台设备可试用的次数
  • 每次试用固定给 1 天(trialTurnDays = 1)
  • 第 1 次调用:开一个 1 天窗口,trialLeft = trialTimes - 1
  • 同一窗口内重复调用:不扣次数,返回同一个 expireAt
  • 窗口过期后再调用:扣 1 次,重新开 1 天
  • 次数用完:code = 220
days 按天
  • trialDays = 每台设备可试用的天数
  • 第 1 次调用:一次性开一个 N 天窗口
  • 窗口内调用多少次都行,不消耗、不续期
  • trialLeft = 剩余天数(按剩余时间向上取整)
  • 窗口到期后再调用:code = 220(试用期已结束)

字段对照

字段times 按次数days 按天
trialEnabled1 开启 / 0 关闭1 开启 / 0 关闭
trialMode"times""days"
trialTimesN(次数)0
trialDays0N(天数)
trialTurnDays1(每次给几天)0
trialLeft剩余次数剩余天数(向上取整)
trialTotal总次数总天数

只读当前模式的那个字段。另一个恒为 0,不要拿 0 当「不限次数」来用。

两种模式的时间轴

按次数(trialTimes = 3,每次 1 天) ├─ 第 1 天 调用 → 开窗到 +1d,trialLeft=2 ├─ 第 1 天内再调用 → 不扣,expireAt 不变,trialLeft=2 ├─ 第 2 天 调用 → 扣 1 次,开窗到 +1d,trialLeft=1 ├─ 第 3 天 调用 → 扣 1 次,开窗到 +1d,trialLeft=0 └─ 第 4 天 调用 → code=220,试用结束 按天(trialDays = 7) ├─ 第 1 天 调用 → 一次性开窗到 +7d,trialLeft=7 ├─ 第 2~7 天内任意次调用 → 不扣、不续期,expireAt 不变 └─ 第 8 天 调用 → code=220,试用期已结束(再点也不会续期)
三条硬规则: ① 试用按机器码计数,machineCode 必传,不传返回 225; ② 试用会话的 user_type = trial,不能拿来调 Recharge / UnBind / ModifyPassword; ③ trialEnabled = 0 时调用 Trial 直接返回 221,客户端应把「试用」按钮置灰。

初始化时的返回(节选)

// 按次数 { "trialEnabled": 1, "trialMode": "times", "trialTimes": 3, "trialDays": 0, "trialTurnDays": 1 } // 按天 { "trialEnabled": 1, "trialMode": "days", "trialTimes": 0, "trialDays": 7, "trialTurnDays": 0 }

Trial 的返回

// 按次数:剩余 2 次 {"code":200,"msg":"试用登录成功","data":{"token":"...","trialMode":"times","trialLeft":2,"trialTotal":3,"trialTurnDays":1,"expireAt":"2026-10-21 19:00:00","leftSecond":86400}} // 按天:还剩 7 天 {"code":200,"msg":"试用登录成功","data":{"token":"...","trialMode":"days","trialLeft":7,"trialTotal":7,"trialDays":7,"expireAt":"2026-10-27 19:00:00","leftSecond":604800}}

兼容说明:旧版本还有 trialHours(每次几小时)字段。为不弄坏已发出去的客户端, Initialize 仍会返回它(按天模式 = 天数 × 24,按次数模式 = 24),但已废弃,新客户端一律改用 trialMode + trialDays / trialTimes。

状态码对照表

外层 status(系统级)

值含义
1成功
0未知错误
-1appid 不存在或已被删除
-2签名校验失败
-3请求已过期(时间戳超出容差)
-4数据包解密失败(安全码不匹配或明文非法)
-5缺少必要参数
-6软件已被停用
-7该接口未开放
-8请求过于频繁
-9IP 不在白名单内
-10系统维护中
-11请求方式错误(需 POST)
-12重放请求(该签名已被使用过)

内层 code(业务级)

值含义
200成功
201帐号不存在
202密码错误
203帐号已被禁用
204帐号已被封停
205帐号已绑定其他机器
206机器码不匹配
207机器码未绑定,请先登录一次
208帐号已过期!
209卡密不存在
210卡密已被使用
211卡密已过期
212卡密已被禁用
213卡密与当前软件不匹配
214卡密需要密码,请填写密码
215卡密密码错误
216用户名已存在
217用户名不存在或格式不正确
218验证码错误或已失效
219旧密码错误
220试用已到期或次数已用完
221试用功能已关闭
222单码登录不支持该操作
223未登录或登录已失效
224解绑次数已用完
225参数错误
226充值失败
227远程函数不存在
228账号点数不足
229操作过于频繁
230用户数据不存在
231机器码被锁定
232该账号已在线(不允许重复登录)
500服务端处理异常

接口明细

data.api 传编号,或 data.func 传接口名,二者等价
api = 0 Initialize 初始化

客户端启动时先调用,拿到 token 与软件配置(版本、公告、试用策略、心跳间隔)。

参数类型必需说明
machineCode文本 否机器码,建议提供,便于后续绑定校验
version文本 否客户端版本号,服务端据此判定是否强制更新
data 解密后的返回示例:
{"code":200,"msg":"初始化成功","data":{"token":"b64e2dad-...","softName":"AndSoo PDF 编辑器","version":"3.2.1","notice":"...","forceUpdate":"","trialEnabled":1,"trialMode":"times","trialTimes":3,"trialDays":0,"trialTurnDays":1,"heartbeat":60,"unbindTimes":3,"serverTime":1549432081000}}
api = 1 Login 帐号登录

用户名 + 密码登录,返回 token 与账号信息。首次登录会自动绑定机器码。

参数类型必需说明
userName文本 是账号
password文本 是密码
machineCode文本 建议机器码;不传则不校验机器
verifyCode文本 否图形验证码,与 machineCode 配对校验
data 解密后的返回示例:
{"code":200,"msg":"登录成功","data":{"userName":"test","token":"...","points":0,"expireAt":"2027-01-01 12:00:00","expireTime":1793505600,"forever":0,"unbindLeft":3,"machineCode":"...","heartbeat":60}}
api = 2 CardLogin 卡密登录

直接用卡密登录(单码登录)。首次使用时自动激活并绑定机器码,按卡密天数计算有效期。

参数类型必需说明
cardNo文本 是卡密,例如 AS7K2M-9F4Q-8XZP
cardPass文本 否卡密密码(后台生成时设置了才需要)
machineCode文本 建议机器码
data 解密后的返回示例:
{"code":200,"msg":"登录成功","data":{"cardNo":"AS7K2M-9F4Q-8XZP","cardType":"month","token":"...","expireAt":"2026-11-14 17:00:00","expireTime":1763...,"machineCode":"..."}}
api = 3 Trial 试用登录

按机器码试用。每款软件只能有一种试用方式:按次数(每次 1 天)或按天(N 天,期内不限次数)。具体看 Initialize 返回的 trialMode。

参数类型必需说明
machineCode文本 是机器码,试用按机器码计数,必须提供
data 解密后的返回示例:
{"code":200,"msg":"试用登录成功","data":{"token":"...","trialMode":"times","trialLeft":2,"trialTotal":3,"trialTimes":3,"trialDays":0,"trialTurnDays":1,"expireAt":"2026-10-21 19:00:00","expireTime":1792580400,"leftSecond":86399}}
api = 5 Register 注册帐号

注册新账号。若同时传 cardNo,则注册后立即用该卡密充值。

参数类型必需说明
userName文本 是账号,3-32 位字母/数字/下划线
password文本 是密码,至少 6 位
machineCode文本 否机器码
verifyCode文本 否验证码
email文本 否邮箱
qq文本 否QQ
cardNo文本 否注册同时充值用的卡密
cardPass文本 否卡密密码
data 解密后的返回示例:
{"code":200,"msg":"注册成功","data":{"userName":"newuser01","token":"...","expireAt":null}}
api = 6 Recharge 帐号充值

用卡密给账号增加时长或点数。时长卡从当前有效期顺延。

参数类型必需说明
cardNo文本 是卡密
cardPass文本 否卡密密码
token文本 二选一登录后拿到的 token
userName文本 二选一账号(与 password 配对使用)
password文本 二选一密码
data 解密后的返回示例:
{"code":200,"msg":"充值成功:有效期至 2027-01-01 12:00:00","data":{"userName":"test","expireAt":"2027-01-01 12:00:00"}}
api = 7 ModifyPassword 修改密码

修改账号密码,成功后该账号所有会话失效,需要重新登录。

参数类型必需说明
newPassword文本 是新密码,至少 6 位
password文本 是旧密码
userName文本 二选一账号
token文本 二选一登录 token
data 解密后的返回示例:
{"code":200,"msg":"密码修改成功,请重新登录"}
api = 4 UnBind 帐号解绑

清除账号绑定的机器码并扣除一次解绑次数,之后可在新机器登录。

参数类型必需说明
token文本 二选一登录 token
userName文本 二选一账号
password文本 二选一密码
data 解密后的返回示例:
{"code":200,"msg":"解绑成功","data":{"userName":"test","unbindLeft":2,"machineCode":""}}
api = 8 Heartbeat 心跳

客户端按服务端下发的心跳间隔定时调用,维持在线状态。返回剩余秒数,被踢或被封会返回对应错误码。

参数类型必需说明
token文本 是登录 token(也可用 cookie)
data 解密后的返回示例:
{"code":200,"msg":"ok","data":{"userName":"test","userType":"account","points":0,"leftSecond":2592000,"heartbeat":60,"serverTime":...}}
api = 9 TelnetFun 远程函数

按名字调用后台配置的远程函数,返回值由服务端决定,客户端拿不到实现。

参数类型必需说明
funName文本 是函数名,在后台「远程函数」里配置
token文本 否登录 token,用于占位符替换
data 解密后的返回示例:
{"code":200,"msg":"ok","data":{"funName":"GetNotice","type":"json","result":{"msg":"请遵守软件使用协议"}}}
api = 10 UserVcode 验证码

生成一次性验证码(5 分钟有效),客户端可自绘成图片,注册/登录时回填校验。

参数类型必需说明
machineCode文本 是机器码
data 解密后的返回示例:
{"code":200,"msg":"ok","data":{"verifyCode":"5183","expireSec":300}}
api = 11 Query 简单查询

不登录也能查:账号是否存在/是否有效、卡密是否可用。适合做"激活前预检"。

参数类型必需说明
userName文本 二选一要查询的账号
cardNo文本 二选一要查询的卡密
machineCode文本 否用于判断机器是否匹配
data 解密后的返回示例:
{"code":200,"msg":"ok","data":{"exists":1,"userName":"test","active":1,"expireAt":"2027-01-01 12:00:00","bound":1,"machineMatch":1}}
api = 12 Get 取登录数据

读取当前登录用户存在服务端的自定义数据(可整套读,也可按 key 读)。

参数类型必需说明
token文本 是登录 token
key文本 否要读取的键名,留空则返回全部
data 解密后的返回示例:
{"code":200,"msg":"ok","data":{"key":"theme","value":"dark","exists":1}}
api = 13 Set 设置用户数据

把客户端数据存在服务端(云端配置、试用标记、统计等)。value 传空字符串表示删除该键。

参数类型必需说明
token文本 是登录 token
key文本 是键名
value文本 否值;传空表示删除
data 解密后的返回示例:
{"code":200,"msg":"保存成功","data":{"data":{"theme":"dark"}}}
api = 14 Exit 退出登录

销毁会话并标记离线。客户端"退出账号"时应调用。

参数类型必需说明
token文本 是登录 token
data 解密后的返回示例:
{"code":200,"msg":"已退出"}

调用示例

PHP

<?php // RC4(标准实现,加解密同一个函数) function rc4($key, $data) { $S = range(0, 255); $j = 0; $klen = strlen($key); for ($i = 0; $i < 256; $i++) { $j = ($j + $S[$i] + ord($key[$i % $klen])) & 0xFF; $t = $S[$i]; $S[$i] = $S[$j]; $S[$j] = $t; } $out = ''; $i = 0; $j = 0; for ($k = 0; $k < strlen($data); $k++) { $i = ($i + 1) & 0xFF; $j = ($j + $S[$i]) & 0xFF; $t = $S[$i]; $S[$i] = $S[$j]; $S[$j] = $t; $out .= chr(ord($data[$k]) ^ $S[($S[$i] + $S[$j]) & 0xFF]); } return $out; } $appid = '你的 appid'; $safe = '你的安全码'; $custom = '你的自定义码'; $apiUrl = 'https://www.andsoo.com/UserApi'; // 1) 组装业务参数(api=3 表示 Trial 试用登录) $req = array('api' => 3, 'machineCode' => 'MY-PC'); $json = json_encode($req, JSON_UNESCAPED_UNICODE); // 2) 加密 + 签名(明文用 GBK 编码) $data = strtoupper(bin2hex(rc4($safe, mb_convert_encoding($json, 'GBK', 'UTF-8')))); $ts = (string)round(microtime(true) * 1000); $sign = md5($appid . $safe . $ts . $data); // 3) 发送 $ch = curl_init($apiUrl); curl_setopt_array($ch, array(CURLOPT_POST => 1, CURLOPT_RETURNTRANSFER => 1, CURLOPT_POSTFIELDS => http_build_query(array('appid' => $appid, 'timestamp' => $ts, 'data' => $data, 'sign' => $sign)))); $resp = json_decode(curl_exec($ch), true); // 4) 解密响应 if ($resp['status'] == 1) { $plain = mb_convert_encoding(rc4($custom, hex2bin(strtolower($resp['data']))), 'UTF-8', 'GBK'); $inner = json_decode($plain, true); if ($inner['code'] == 200) { // 按模式处理:只看当前模式的那个字段 if ($inner['data']['trialMode'] === 'days') { echo '还剩 ' . $inner['data']['trialLeft'] . ' 天'; } else { echo '还剩 ' . $inner['data']['trialLeft'] . ' 次'; } } else { echo $inner['msg']; } } else { echo '系统错误:' . $resp['status']; }

Python 3

import json, time, hashlib, urllib.request, urllib.parse def rc4(key: bytes, data: bytes) -> bytes: S = list(range(256)); j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) & 0xFF S[i], S[j] = S[j], S[i] out = bytearray(); i = j = 0 for ch in data: i = (i + 1) & 0xFF; j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] out.append(ch ^ S[(S[i] + S[j]) & 0xFF]) return bytes(out) APPID, SAFE, CUSTOM = '你的 appid', '你的安全码', '你的自定义码' URL = 'https://www.andsoo.com/UserApi' body = json.dumps({"api": 3, "machineCode": "MY-PC"}, ensure_ascii=False) data = rc4(SAFE.encode(), body.encode('gbk')).hex().upper() ts = str(int(time.time() * 1000)) sign = hashlib.md5((APPID + SAFE + ts + data).encode()).hexdigest() post = urllib.parse.urlencode({"appid": APPID, "timestamp": ts, "data": data, "sign": sign}).encode('gbk') req = urllib.request.Request(URL, post, {"Content-Type": "application/x-www-form-urlencoded; charset=GBK"}) resp = json.loads(urllib.request.urlopen(req, timeout=15).read()) if resp["status"] == 1: inner = json.loads(rc4(CUSTOM.encode(), bytes.fromhex(resp["data"])).decode('gbk')) print(inner["code"], inner["msg"]) else: print("系统错误", resp["status"], resp["msg"])

C#

// 关键点:用 Encoding.GetEncoding("GBK") 处理明文,Hex 大写 static byte[] Rc4(byte[] key, byte[] data) { var S = Enumerable.Range(0, 256).ToArray(); int j = 0; for (int i = 0; i < 256; i++) { j = (j + S[i] + key[i % key.Length]) & 0xFF; (S[i], S[j]) = (S[j], S[i]); } var outp = new byte[data.Length]; int a = 0, b = 0; for (int k = 0; k < data.Length; k++) { a = (a + 1) & 0xFF; b = (b + S[a]) & 0xFF; (S[a], S[b]) = (S[b], S[a]); outp[k] = (byte)(data[k] ^ S[(S[a] + S[b]) & 0xFF]); } return outp; } // 加密: var gbk = System.Text.Encoding.GetEncoding("GBK"); string data = BitConverter.ToString(Rc4(gbk.GetBytes(safeCode), gbk.GetBytes(json))) .Replace("-", ""); // 签名: string sign = Md5Hex(appid + safeCode + timestamp + data).ToLower();

易语言

.版本 2 .子程序 RC4加密, 字节集, 公开, 加解密同一个子程序 ' 1. 初始化 S 盒:S[i] = i ' 2. 用密钥打乱:j = (j + S[i] + 密钥[i % 密钥长度]) % 256,交换 S[i] 与 S[j] ' 3. 生成密钥流:i=(i+1)%256; j=(j+S[i])%256; 交换; k = S[(S[i]+S[j])%256] ' 4. 密文[i] = 明文[i] 异或 k .子程序 组装请求, 文本型 ' 明文 = 到文本(请求JSON) ' 易语言默认就是 GBK 编码,无需转换 ' data = 到大写(字节集到十六进制(RC4加密(安全码, 明文))) ' ts = 到文本(取时间戳()) ' 毫秒 ' sign = 取数据摘要(到字节集(appid + 安全码 + ts + data)) ' 32位小写 MD5 .子程序 解析响应 ' 明文 = RC4加密(自定义码, 十六进制到字节集(响应.data)) ' 内层 = JSON解析(明文) ' 取 内层.code 判断是否 200

易语言的字符串本身就是 GBK 字节串,所以不需要额外转码,直接用「到字节集」即可对上服务端。 这也是协议当初选择 GBK 的原因之一。

常见问题

status = -2(签名校验失败)
九成是 sign 拼错。检查顺序必须是 appid + 安全码 + timestamp + data密文, 中间不能加分隔符,且要用加密后的 data。另外注意 appid 要当字符串拼,不要带小数点。

status = -3(请求已过期)
客户端机器时间不准。可用 Initialize 返回的 serverTime 校准本地时钟偏移, 或把后台「时间戳容差」调大一些。

status = -4(数据包解密失败)
安全码不匹配,或明文不是合法 JSON。注意明文必须是 GBK 编码后再加密。

status = -12(重放请求)
同一个数据包发了两遍。每次请求都要重新生成 timestamp 与 data(timestamp 变了密文自然不同)。

中文乱码
两种可能:① 明文没有用 GBK 编码;② HTTP 头没写 charset=GBK。 本服务端在解密后会自动做 GBK→UTF-8 修正,所以多数情况是客户端侧没按 GBK 发送。

试用还剩几次 / 几天,该读哪个字段?
只看 trialLeft,单位由 trialMode 决定:times 是次数, days 是天数。trialTimes 与 trialDays 里有一个恒为 0, 那是「当前模式没用到」,不是「不限」。

为什么试用期到了再点试用也没反应?
按天模式只开一次窗,重复调用不会续期(否则改天再点一下就能永久试用)。 这时服务端返回 code = 220,客户端应引导用户去购买卡密。

客户端怎么知道该调用哪个接口?
用 data.api 传编号(见上方接口明细),或用 data.func 传接口名, 两种方式等价,推荐用编号更省流量。