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 不一致,最终返回非法请求。
为什么以前正常,后来突然不正常
常见触发点有几个:
- 某次 GM_xmlhttpRequest 请求时 token 失效,服务端给扩展分区重新 Set-Cookie
- Chrome / Tampermonkey 更新后,分区 cookie 行为更严格或更完整
- 浏览器隐私设置、第三方 cookie、Privacy Sandbox 行为变化
- 只清了网页站点 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. 建议优先级
- 优先使用页面 JSONP
<script>方式发请求 - 如果必须使用
GM_xmlhttpRequest,则读取扩展分区_m_h5_tk并用该 token 计算sign - 删除扩展分区 cookie 只作为临时修复或排障手段
6. 注意事项
- 不要把完整 cookie、登录态、证件号、手机号等敏感信息发给他人
- 日志中只打印 token 前 8 位用于排查即可
fliggy.com下的_m_h5_tk不会自动发送给h5api.m.taobao.com,通常不是本次问题的直接来源- 清理 cookie 时需要关注
Domain、Path、Partition Key,只清网页可见 cookie 可能无法清掉扩展分区 cookie