MSY 设备试用接口
为设备发放一次试用时长,重复调用返回当前剩余秒数。
为设备发放一次试用时长,重复调用返回当前剩余秒数。
https://yunzhuru.cn/msy/trial.php| 参数 | 必填 | 说明 |
|---|---|---|
appid | 是 | 原始应用APPID |
uid | 是 | 应用所属用户UID |
package | 是 | 应用包名 |
did | 是 | 非空设备唯一标识 |
minutes | 是 | 试用分钟数,范围1至999999999 |
key | 是 | 应用API Key |
{
"code": 200,
"message": "ok",
"data": {
"remaining_seconds": 600
},
"appid": "1001",
"time": 1785225600,
"nonce": "0123456789abcdef0123456789abcdef",
"sign": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}所有接口真实返回字段固定为:
code:业务状态码。message:业务说明。data:本接口业务数据。appid:参与签名的请求 APPID。time:服务端 Unix 时间戳。nonce:32位随机十六进制字符串。sign:64位 HMAC-SHA256 小写十六进制签名。data:对象键名升序,数组保持顺序。appid + "\n" + time + "\n" + nonce + "\n" + canonicalJson(data)。curl -X POST "https://yunzhuru.cn/msy/trial.php" \
+ -d "appid=1001" -d "uid=2001" -d "package=com.example.app" \
+ -d "did=device-unique-id" -d "minutes=10" -d "key=替换为应用API_KEY"const apiKey = '替换为应用API_KEY';
const request = {
"appid": "1001",
"uid": "2001",
"package": "com.example.app",
"did": "device-unique-id",
"minutes": "10",
"key": "由代码中的apiKey自动填入",
key: apiKey
};
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (value && typeof value === 'object') {
return Object.keys(value).sort().reduce((result, key) => {
result[key] = canonicalize(value[key]);
return result;
}, {});
}
return value;
}
function toHex(buffer) {
return [...new Uint8Array(buffer)].map(byte => byte.toString(16).padStart(2, '0')).join('');
}
const response = await fetch('https://yunzhuru.cn/msy/trial.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},
body: new URLSearchParams(request)
});
const result = await response.json();
if (String(result.appid) !== String(request.appid)) throw new Error('APPID不一致');
if (Math.abs(Date.now() / 1000 - Number(result.time)) > 300) throw new Error('响应已过期');
const canonical = `${result.appid}\n${result.time}\n${result.nonce}\n${JSON.stringify(canonicalize(result.data))}`;
const cryptoKey = await crypto.subtle.importKey('raw', new TextEncoder().encode(apiKey), {name: 'HMAC', hash: 'SHA-256'}, false, ['sign']);
const expected = toHex(await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(canonical)));
if (expected !== String(result.sign).toLowerCase()) throw new Error('响应验签失败');
if (Number(result.code) !== 200) throw new Error(result.message);
console.log(result.data);<?php
declare(strict_types=1);
$apiKey = '替换为应用API_KEY';
$request = [
'appid' => '1001',
'uid' => '2001',
'package' => 'com.example.app',
'did' => 'device-unique-id',
'minutes' => '10',
'key' => '由代码中的apiKey自动填入'
];
$request['key'] = $apiKey;
function isListArray(array $value): bool
{
$index = 0;
foreach ($value as $key => $_) {
if ($key !== $index++) return false;
}
return true;
}
function canonicalize($value)
{
if (!is_array($value)) return $value;
if (isListArray($value)) {
foreach ($value as $key => $item) $value[$key] = canonicalize($item);
return $value;
}
ksort($value, SORT_STRING);
foreach ($value as $key => $item) $value[$key] = canonicalize($item);
return $value;
}
$curl = curl_init('https://yunzhuru.cn/msy/trial.php');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($request),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($curl);
if ($body === false) throw new RuntimeException(curl_error($curl));
curl_close($curl);
$response = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ((string)$response['appid'] !== (string)$request['appid']) throw new RuntimeException('APPID不一致');
if (abs(time() - (int)$response['time']) > 300) throw new RuntimeException('响应已过期');
$dataJson = json_encode(canonicalize($response['data']), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$canonical = $response['appid'] . "\n" . $response['time'] . "\n" . $response['nonce'] . "\n" . $dataJson;
$expected = hash_hmac('sha256', $canonical, $apiKey);
if (!hash_equals($expected, (string)$response['sign'])) throw new RuntimeException('响应验签失败');
if ((int)$response['code'] !== 200) throw new RuntimeException((string)$response['message']);
$data = $response['data'];
print_r($data);// Maven依赖:com.google.code.gson:gson:2.11.0
import com.google.gson.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
public class MsyApiExample {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private static String canonicalJson(JsonElement value) {
if (value == null || value.isJsonNull()) return "null";
if (value.isJsonArray()) {
List<String> items = new ArrayList<>();
for (JsonElement item : value.getAsJsonArray()) items.add(canonicalJson(item));
return "[" + String.join(",", items) + "]";
}
if (value.isJsonObject()) {
TreeMap<String, JsonElement> sorted = new TreeMap<>();
value.getAsJsonObject().entrySet().forEach(entry -> sorted.put(entry.getKey(), entry.getValue()));
return sorted.entrySet().stream()
.map(entry -> GSON.toJson(entry.getKey()) + ":" + canonicalJson(entry.getValue()))
.collect(Collectors.joining(",", "{", "}"));
}
return GSON.toJson(value);
}
private static String hmacSha256(String key, String text) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] bytes = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte value : bytes) hex.append(String.format("%02x", value & 0xff));
return hex.toString();
}
public static void main(String[] args) throws Exception {
String apiKey = "替换为应用API_KEY";
Map<String, String> form = new LinkedHashMap<>();
form.put("appid", "1001");
form.put("uid", "2001");
form.put("package", "com.example.app");
form.put("did", "device-unique-id");
form.put("minutes", "10");
form.put("key", "由代码中的apiKey自动填入");
form.put("key", apiKey);
String body = form.entrySet().stream()
.map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" +
URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
HttpRequest request = HttpRequest.newBuilder(URI.create("https://yunzhuru.cn/msy/trial.php"))
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(body)).build();
String json = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()).body();
JsonObject response = JsonParser.parseString(json).getAsJsonObject();
String appid = response.get("appid").getAsString();
long time = response.get("time").getAsLong();
if (!appid.equals(form.get("appid"))) throw new SecurityException("APPID不一致");
if (Math.abs(Instant.now().getEpochSecond() - time) > 300) throw new SecurityException("响应已过期");
String canonical = appid + "\n" + time + "\n" + response.get("nonce").getAsString() +
"\n" + canonicalJson(response.get("data"));
String expected = hmacSha256(apiKey, canonical);
if (!MessageDigest.isEqual(expected.getBytes(StandardCharsets.US_ASCII),
response.get("sign").getAsString().toLowerCase().getBytes(StandardCharsets.US_ASCII)))
throw new SecurityException("响应验签失败");
if (response.get("code").getAsInt() != 200) throw new RuntimeException(response.get("message").getAsString());
System.out.println(GSON.toJson(response.get("data")));
}
}-- 依赖:luasec、lua-cjson、luaossl
local https = require('ssl.https')
local ltn12 = require('ltn12')
local cjson = require('cjson')
local openssl_hmac = require('openssl.hmac')
local api_key = '替换为应用API_KEY'
local form = {
appid = '1001',
uid = '2001',
package = 'com.example.app',
did = 'device-unique-id',
minutes = '10',
key = '由代码中的apiKey自动填入',
key = api_key
}
local function urlencode(value)
return tostring(value):gsub('\n', '\r\n'):gsub('([^%w%-_%.~])', function(char)
return string.format('%%%02X', string.byte(char))
end)
end
local function canonical_json(value)
if type(value) ~= 'table' then return cjson.encode(value) end
local count, max = 0, 0
for key in pairs(value) do
if type(key) ~= 'number' or key < 1 or key % 1 ~= 0 then count = -1 break end
count = count + 1; if key > max then max = key end
end
if count >= 0 and count == max then
local items = {}; for index = 1, max do items[index] = canonical_json(value[index]) end
return '[' .. table.concat(items, ',') .. ']'
end
local keys = {}; for key in pairs(value) do keys[#keys + 1] = key end; table.sort(keys)
local items = {}; for _, key in ipairs(keys) do items[#items + 1] = cjson.encode(key) .. ':' .. canonical_json(value[key]) end
return '{' .. table.concat(items, ',') .. '}'
end
local pairs_list = {}; for key, value in pairs(form) do pairs_list[#pairs_list + 1] = urlencode(key) .. '=' .. urlencode(value) end
local post = table.concat(pairs_list, '&'); local chunks = {}
local _, status = https.request{url='https://yunzhuru.cn/msy/trial.php',method='POST',
headers={['content-type']='application/x-www-form-urlencoded;charset=UTF-8',['content-length']=#post},
source=ltn12.source.string(post),sink=ltn12.sink.table(chunks)}
assert(status == 200, 'HTTP请求失败: ' .. tostring(status))
local response = cjson.decode(table.concat(chunks))
assert(tostring(response.appid) == tostring(form.appid), 'APPID不一致')
assert(math.abs(os.time() - tonumber(response.time)) <= 300, '响应已过期')
local canonical = response.appid .. '\n' .. response.time .. '\n' .. response.nonce .. '\n' .. canonical_json(response.data)
local ctx = assert(openssl_hmac.new(api_key, 'sha256')); ctx:update(canonical)
local expected = (ctx:final()):gsub('.', function(char) return string.format('%02x', string.byte(char)) end)
assert(expected == string.lower(response.sign), '响应验签失败')
assert(response.code == 200, response.message)
print(canonical_json(response.data))// pubspec.yaml: http: ^1.2.2, crypto: ^3.0.6
import 'dart:collection';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
dynamic canonicalize(dynamic value) {
if (value is List) return value.map(canonicalize).toList();
if (value is Map) {
final sorted = SplayTreeMap<String, dynamic>();
value.forEach((key, item) => sorted[key.toString()] = canonicalize(item));
return sorted;
}
return value;
}
bool constantTimeEquals(String left, String right) {
if (left.length != right.length) return false;
var difference = 0;
for (var index = 0; index < left.length; index++) {
difference |= left.codeUnitAt(index) ^ right.codeUnitAt(index);
}
return difference == 0;
}
Future<void> main() async {
const apiKey = '替换为应用API_KEY';
final request = <String, String>{
'appid': '1001',
'uid': '2001',
'package': 'com.example.app',
'did': 'device-unique-id',
'minutes': '10',
'key': '由代码中的apiKey自动填入',
'key': apiKey
};
final httpResponse = await http.post(Uri.parse('https://yunzhuru.cn/msy/trial.php'), body: request);
if (httpResponse.statusCode != 200) throw Exception('HTTP错误:${httpResponse.statusCode}');
final response = jsonDecode(httpResponse.body) as Map<String, dynamic>;
if (response['appid'].toString() != request['appid']) throw Exception('APPID不一致');
final time = int.parse(response['time'].toString());
if ((DateTime.now().millisecondsSinceEpoch ~/ 1000 - time).abs() > 300) throw Exception('响应已过期');
final dataJson = jsonEncode(canonicalize(response['data']));
final canonical = '${response['appid']}\n${response['time']}\n${response['nonce']}\n$dataJson';
final expected = Hmac(sha256, utf8.encode(apiKey)).convert(utf8.encode(canonical)).toString();
if (!constantTimeEquals(expected, response['sign'].toString().toLowerCase())) throw Exception('响应验签失败');
if (int.parse(response['code'].toString()) != 200) throw Exception(response['message'].toString());
print(jsonEncode(response['data']));
}// iApp调用代码;先把下方Java类编译为插件/模块并导入
s key = "替换为应用API_KEY"
s post = "appid=1001&uid=2001&package=com.example.app&did=device-unique-id&minutes=10&key=由代码中的apiKey自动填入&key=" + key
hs("https://yunzhuru.cn/msy/trial.php", post, "utf-8", null, true, result)
// 通过你导入的Java桥接调用:MsyResponseVerifier.verify(result, key, "1001")
s verifyResult = MsyResponseVerifier.verify(result, key, "1001")
f(verifyResult == "ok")
{
t("验签成功")
}
else
{
t(verifyResult)
}// Android内置org.json可用;将本类编译为iApp可调用的Java插件
import org.json.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
public final class MsyResponseVerifier {
private static String canonical(Object value) throws Exception {
if (value == null || value == JSONObject.NULL) return "null";
if (value instanceof JSONArray) {
JSONArray array = (JSONArray)value; StringBuilder out = new StringBuilder("[");
for (int i=0;i<array.length();i++){if(i>0)out.append(',');out.append(canonical(array.get(i)));}
return out.append(']').toString();
}
if (value instanceof JSONObject) {
JSONObject object=(JSONObject)value; List<String> keys=new ArrayList<>();
Iterator<String> iterator=object.keys(); while(iterator.hasNext())keys.add(iterator.next());
Collections.sort(keys); StringBuilder out=new StringBuilder("{");
for(int i=0;i<keys.size();i++){if(i>0)out.append(',');String key=keys.get(i);out.append(JSONObject.quote(key)).append(':').append(canonical(object.get(key)));}
return out.append('}').toString();
}
if (value instanceof String) return JSONObject.quote((String)value);
return String.valueOf(value);
}
public static String verify(String body,String apiKey,String requestAppid){
try{
JSONObject response=new JSONObject(body);String appid=response.getString("appid");
long time=response.getLong("time");if(!appid.equals(requestAppid))return "APPID不一致";
if(Math.abs(System.currentTimeMillis()/1000-time)>300)return "响应已过期";
String text=appid+"\n"+time+"\n"+response.getString("nonce")+"\n"+canonical(response.opt("data"));
Mac mac=Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8),"HmacSHA256"));
byte[] raw=mac.doFinal(text.getBytes(StandardCharsets.UTF_8));StringBuilder hex=new StringBuilder();
for(byte item:raw)hex.append(String.format("%02x",item&0xff));
if(!MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.US_ASCII),response.getString("sign").toLowerCase().getBytes(StandardCharsets.US_ASCII)))return "响应验签失败";
return response.getInt("code")==200?"ok":response.optString("message","接口失败");
}catch(Exception error){return "解析失败:"+error.getMessage();}
}
}