听ta说

  • MTop 接口非法请求问题排查记录
  • 1. 遇到问题
  • 2. 造成原因
  • 3. 测试找因
  • 3.1 对比 929 与 931 请求
  • 3.2 校验 sign
  • 3.3 使用 GM_cookie.list 枚举分区 cookie
  • 4. 解决方案
  • 4.1 推荐方案:使用页面 JSONP script 发起请求
  • 4.2 继续使用 GM_xmlhttpRequest:读取扩展分区 token 计算 sign
  • 4.3 临时方案:删除扩展分区里的 MTop cookie
  • 5. 建议优先级
  • 6. 注意事项
  • 首页

油猴请求MTop 接口,报非法请求的问题排查记录

  • admin
  • 2026-05-18
  • 0

MTop 接口非法请求问题排查记录

1. 遇到问题

同一个飞猪 MTop 接口 fliggy.combined.passenger.list,网站自身发起的请求可以正常返回数据,但油猴脚本发起的请求返回:

{
  "ret": [
    "FAIL_SYS_ILLEGAL_ACCESS::非法请求"
  ]
}

已知现象:

  • 931.txt 是网站发起的请求,可以正常成功
  • 929.txt 是油猴脚本发起的请求,返回非法请求
  • 同一份油猴脚本换浏览器后可以正常运行
  • 当前浏览器以前也能正常运行,后续突然出现问题
  • 清空常见相关 cookie 并重新登录后,问题仍然存在

2. 造成原因

直接原因是:油猴请求实际携带的 _m_h5_tk token 与脚本用于计算 sign 的 _m_h5_tk token 不一致。

MTop H5 接口常见签名计算方式为:

sign = md5(token + "&" + t + "&" + appKey + "&" + data)

其中:

  • token 来自 _m_h5_tk 中下划线前面的部分
  • t 来自 URL 参数
  • appKey 来自 URL 参数
  • data 来自 URL 参数

本次问题中,脚本通过 document.cookie 读取到的是网页正常上下文里的 _m_h5_tk,并用它计算 sign。但油猴实际发起请求时,浏览器携带的是 Tampermonkey 扩展分区里的另一条 _m_h5_tk。

因此服务端校验时使用“请求实际携带的 token”重新计算 sign,结果与 URL 中的 sign 不一致,最终返回非法请求。

为什么以前正常,后来突然不正常

常见触发点有几个:

  1. 某次 GM_xmlhttpRequest 请求时 token 失效,服务端给扩展分区重新 Set-Cookie
  2. Chrome / Tampermonkey 更新后,分区 cookie 行为更严格或更完整
  3. 浏览器隐私设置、第三方 cookie、Privacy Sandbox 行为变化
  4. 只清了网页站点 cookie,没有清到 chrome-extension 分区 cookie

3. 测试找因

3.1 对比 929 与 931 请求

两个请求的 cookie 名称基本一致,但请求上下文不同。

油猴请求 929.txt 的特征:

sec-fetch-site: none
sec-fetch-mode: cors
sec-fetch-dest: empty

网站请求 931.txt 的特征:

sec-fetch-site: same-site
sec-fetch-mode: no-cors
sec-fetch-dest: script

这说明网站请求更像通过 JSONP <script> 发起,而油猴请求更像通过 GM_xmlhttpRequest 或扩展网络上下文发起。

3.2 校验 sign

使用 931.txt 中的 _m_h5_tk、t、appKey、data 重新计算 sign,结果与 URL 中的 sign 完全一致,所以 931.txt 请求成功。

使用 929.txt 请求实际携带的 _m_h5_tk 重新计算 sign,结果与 URL 中的 sign 不一致,所以 929.txt 请求失败。

进一步交叉验证发现:929.txt URL 中的 sign 可以由网页上下文里的旧 token 算出,但请求实际携带的却是另一条 token。

3.3 使用 GM_cookie.list 枚举分区 cookie

通过 GM_cookie.list 查询 _m_h5_tk 后发现存在两条同名 cookie:

prefix: e493ed35
domain: .taobao.com
path: /
partitionKey: {"hasCrossSiteAncestor":false,"topLevelSite":"https://taobao.com"}
prefix: 580f3212
domain: .taobao.com
path: /
partitionKey: {"hasCrossSiteAncestor":true,"topLevelSite":"chrome-extension://dhdgffkkebhmkfjojejmpbldmpobfkfo"}

其中 dhdgffkkebhmkfjojejmpbldmpobfkfo 是 Tampermonkey 的 Chrome 扩展 ID。

结论:

  • document.cookie 读取的是网页上下文 token,例如 e493ed35...
  • GM_xmlhttpRequest 实际请求携带的是扩展分区 token,例如 580f3212... 或后续出现的 cf418c8a...
  • 脚本用网页 token 算 sign,但请求用扩展分区 token 发出,导致签名不匹配

4. 解决方案

4.1 推荐方案:使用页面 JSONP script 发起请求

这个接口本身带有 callback 参数,网站请求也表现为:

sec-fetch-mode: no-cors
sec-fetch-dest: script

因此更推荐在页面上下文中插入 <script> 发起 JSONP 请求,让请求行为接近网站自身调用方式。

示例:

// @grant unsafeWindow
// @match https://outfliggys.m.taobao.com/*

function getMtopTokenFromDocumentCookie() {
  const match = document.cookie.match(/(?:^|;\s*)_m_h5_tk=([^;]+)/);
  if (!match) {
    throw new Error("未找到 _m_h5_tk");
  }

  return decodeURIComponent(match[1]).split("_")[0];
}

function callMtopByJsonp(dataObj) {
  return new Promise((resolve, reject) => {
    const appKey = "12574478";
    const t = String(Date.now());
    const data = JSON.stringify(dataObj);
    const token = getMtopTokenFromDocumentCookie();
    const sign = md5(<code class="prettyprint" >${token}&${t}&${appKey}&${data});
    const callback = mtop_cback_${t}_${Math.floor(Math.random() * 100000)};
    const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;

    const url = new URL("https://h5api.m.taobao.com/h5/fliggy.combined.passenger.list/1.0/");
    url.searchParams.set("type", "originaljsonp");
    url.searchParams.set("data", data);
    url.searchParams.set("needLogin", "true");
    url.searchParams.set("ttid", "201300@travel_h5_3.1.0");
    url.searchParams.set("appKey", appKey);
    url.searchParams.set("t", t);
    url.searchParams.set("sign", sign);
    url.searchParams.set("callback", callback);

    const script = document.createElement("script");

    function cleanup() {
      delete pageWindow[callback];
      script.remove();
    }

    pageWindow[callback] = response => {
      cleanup();
      resolve(response);
    };

    script.onerror = () => {
      cleanup();
      reject(new Error("JSONP 请求失败"));
    };

    script.src = url.toString();
    document.head.appendChild(script);
  });
}

注意:md5() 使用脚本现有的签名方法即可。

4.2 继续使用 GM_xmlhttpRequest:读取扩展分区 token 计算 sign

如果必须继续使用 GM_xmlhttpRequest,就不能再用 document.cookie 中的 token 计算 sign,而应该读取 Tampermonkey 扩展分区里的 _m_h5_tk。

示例:

// @grant GM_cookie
// @grant GM_xmlhttpRequest
// @connect h5api.m.taobao.com
// @match https://outfliggys.m.taobao.com/*

function gmCookieList(details) {
  return new Promise((resolve, reject) => {
    GM_cookie.list(details, (cookies, error) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(cookies);
    });
  });
}

async function getTampermonkeyPartitionToken() {
  const cookies = await gmCookieList({
    url: "https://h5api.m.taobao.com/",
    name: "_m_h5_tk",
    partitionKey: {}
  });

  const cookie = cookies.find(item =>
    item.partitionKey?.topLevelSite?.startsWith("chrome-extension://")
  );

  if (!cookie) {
    throw new Error("未找到 Tampermonkey 扩展分区 _m_h5_tk");
  }

  return {
    token: decodeURIComponent(cookie.value).split("_")[0],
    partitionKey: cookie.partitionKey,
    prefix: cookie.value.slice(0, 8)
  };
}

发请求时,需要用扩展分区 token 计算 sign,并显式指定 cookiePartition:

async function callMtopByGmRequest(dataObj) {
  const appKey = "12574478";
  const t = String(Date.now());
  const data = JSON.stringify(dataObj);
  const tk = await getTampermonkeyPartitionToken();
  const sign = md5(<code class="prettyprint" >${tk.token}&${t}&${appKey}&${data});

  const url = new URL("https://h5api.m.taobao.com/h5/fliggy.combined.passenger.list/1.0/");
  url.searchParams.set("type", "originaljsonp");
  url.searchParams.set("data", data);
  url.searchParams.set("needLogin", "true");
  url.searchParams.set("ttid", "201300@travel_h5_3.1.0");
  url.searchParams.set("appKey", appKey);
  url.searchParams.set("t", t);
  url.searchParams.set("sign", sign);
  url.searchParams.set("callback", mtop_cback_${t}_${Math.floor(Math.random() * 100000)});

  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: "GET",
      url: url.toString(),
      cookiePartition: tk.partitionKey,
      onload: response => resolve(response.responseText),
      onerror: reject
    });
  });
}

关键原则:

用哪个 cookie 分区发请求,就必须用哪个 cookie 分区的 _m_h5_tk 计算 sign

4.3 临时方案:删除扩展分区里的 MTop cookie

可以删除 Tampermonkey 扩展分区中的 _m_h5_tk 和 _m_h5_tk_enc,让扩展分区重新生成 token。

示例:

// @grant GM_cookie
// @match https://outfliggys.m.taobao.com/*

function gmCookieList(details) {
  return new Promise((resolve, reject) => {
    GM_cookie.list(details, (cookies, error) => {
      if (error) {
        reject(error);
        return;
      }

      resolve(cookies);
    });
  });
}

function gmCookieDelete(details) {
  return new Promise((resolve, reject) => {
    GM_cookie.delete(details, error => {
      if (error) {
        reject(error);
        return;
      }

      resolve();
    });
  });
}

async function deleteTampermonkeyMtopCookies() {
  const names = ["_m_h5_tk", "_m_h5_tk_enc"];

  for (const name of names) {
    const cookies = await gmCookieList({
      url: "https://h5api.m.taobao.com/",
      name,
      partitionKey: {}
    });

    const targets = cookies.filter(item =>
      item.partitionKey?.topLevelSite?.startsWith("chrome-extension://")
    );

    for (const cookie of targets) {
      console.log("delete cookie:", {
        name: cookie.name,
        prefix: String(cookie.value || "").slice(0, 8),
        domain: cookie.domain,
        path: cookie.path,
        partitionKey: cookie.partitionKey
      });

      await gmCookieDelete({
        url: "https://h5api.m.taobao.com/",
        name: cookie.name,
        partitionKey: cookie.partitionKey
      });
    }
  }

  console.log("Tampermonkey 扩展分区 MTop cookie 已删除,刷新页面后再试");
}

该方案只能清理当前异常 cookie,不能从根本上避免后续再次出现分区 token 与网页 token 不一致。

5. 建议优先级

  1. 优先使用页面 JSONP <script> 方式发请求
  2. 如果必须使用 GM_xmlhttpRequest,则读取扩展分区 _m_h5_tk 并用该 token 计算 sign
  3. 删除扩展分区 cookie 只作为临时修复或排障手段

6. 注意事项

  • 不要把完整 cookie、登录态、证件号、手机号等敏感信息发给他人
  • 日志中只打印 token 前 8 位用于排查即可
  • fliggy.com 下的 _m_h5_tk 不会自动发送给 h5api.m.taobao.com,通常不是本次问题的直接来源
  • 清理 cookie 时需要关注 Domain、Path、Partition Key,只清网页可见 cookie 可能无法清掉扩展分区 cookie
© 2026 听ta说
Theme by Wing
  • {{ item.name }}
  • {{ item.name }}