MSY 应用公开配置接口

MSY 应用公开配置接口

获取应用的公开弹窗、HTML、输入框和远程DEX配置。

接口信息

POST https://yunzhuru.cn/msy/index.php
必须使用原始应用的 APPID、UID、包名和 API Key;生产环境必须通过 HTTPS 调用。

请求参数

参数必填说明
appid原始应用APPID
uid应用所属用户UID
package应用包名
did非空设备唯一标识
key应用API Key

真实响应结构

{
    "code": 200,
    "message": "success",
    "data": {
        "version": "1.0.0",
        "enablePopups": true,
        "popups": [],
        "enableImagePopups": false,
        "imagepopups": [],
        "enablehtmlPopups": false,
        "htmlpopups": [],
        "enableMessagePopups": false,
        "Messagepopups": [],
        "enableinputPopups": false,
        "inputpopups": [],
        "enabledex": false,
        "dex_list": []
    },
    "appid": "1001",
    "time": 1785225600,
    "nonce": "0123456789abcdef0123456789abcdef",
    "sign": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

所有接口真实返回字段固定为:

  • code:业务状态码。
  • message:业务说明。
  • data:本接口业务数据。
  • appid:参与签名的请求 APPID。
  • time:服务端 Unix 时间戳。
  • nonce:32位随机十六进制字符串。
  • sign:64位 HMAC-SHA256 小写十六进制签名。

验签算法

  1. 递归处理 data:对象键名升序,数组保持顺序。
  2. 生成无空白、UTF-8、斜杠不转义的紧凑 JSON。
  3. 拼接:appid + "\n" + time + "\n" + nonce + "\n" + canonicalJson(data)
  4. 使用 API Key 作为密钥计算 HMAC-SHA256。
  5. 同时验证 APPID 相等及时间偏差不超过300秒。

可直接复制的完整调用示例

curl -X POST "https://yunzhuru.cn/msy/index.php" \
+  -d "appid=1001" -d "uid=2001" -d "package=com.example.app" \
+  -d "did=device-unique-id" -d "key=替换为应用API_KEY"
const apiKey = '替换为应用API_KEY';
const request = {
  "appid": "1001",
  "uid": "2001",
  "package": "com.example.app",
  "did": "device-unique-id",
  "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/index.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',
    '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/index.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("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/index.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',
  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/index.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',
    'key': '由代码中的apiKey自动填入',
    'key': apiKey
  };
  final httpResponse = await http.post(Uri.parse('https://yunzhuru.cn/msy/index.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&key=由代码中的apiKey自动填入&key=" + key
hs("https://yunzhuru.cn/msy/index.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();}
    }
}