
美团外卖 H5 签名逆向工程实录
目标: `https://h5.waimai.meituan.com/waimai/mindex/home` (美团外卖 H5 首页)
工具: Chrome DevTools Protocol + js-reverse MCP + Python
日期: 2026-05-15
1. 侦察 —— 安全架构总览
打开目标 URL,页面是一个标准 SPA(Single Page Application)。通过 Chrome DevTools 监控所有脚本和网络请求,识别出美团外卖 H5 的多层安全防护体系。
1.1 安全 SDK 全景
| SDK | URL | 功能 |
|---|---|---|
| **H5guard.js** | `s3plus.meituan.net/…/h5guard/H5guard.js` (343 KB) | 核心签名与请求拦截 |
| **H5guard Package** | `msp.meituan.net/h5guard-files/package-1.1.2.js` (241 KB) | H5guard 扩展模块 |
| **Yoda Seed** | `s0.meituan.net/mxx/yoda/yoda.seed.js` (24 KB) | 风险验证 & 人机识别 SDK |
| **OWL** | `www.dpfile.com/app/owl/static/owl_1.10.1.js` | 设备指纹采集 |
| **Rohr** | `s0.meituan.net/mx/rohr/rohr.min.js` | 数据上报 SDK |
| **LX** | `lx.meituan.net/lx.js` | 用户行为追踪 |
| **Logan** | `s3.meituan.net/…/logan-websdk/logan_2.1.5.js` | 客户端日志系统 |
| **Perf** | `s3.meituan.net/mnpm-cdn/@mtfe-perf-h5-1.8.0/web.fsp.min.js` | 性能监控 |
1.2 业务脚本架构
| 文件 | 大小 | 功能 |
|---|---|---|
| `runtime.fcfe3512.js` | Webpack runtime | 模块加载器 |
| `vendor.568501c4.js` | 第三方依赖 | Vue/BetterScroll 等 |
| `common.0a2863a5.js` | 135 KB | **全局 AJAX 封装 + 签名拦截** |
| `index.9ebc1220.js` | 925 KB | 全局组件与路由 |
| `home.0c4f4a88.js` | 195 KB | **首页业务逻辑** |
2. mtgsig 签名机制 —— 核心逆向
2.1 签名发起流程
业务代码通过 jQuery AJAX 发送请求时,common.0a2863a5.js 中的 XHR 包装器在请求打开之前调用 H5guard.sign() 进行签名:
// common.0a2863a5.js (解混淆)
if (window.top === window) {
o.method = o.type || "GET";
H5guard.sign(o).then(function(t) {
if (!e.guardIngnore &&
o.url.match(/\.waimai\.test\.sankuai|\.waimai\.st\.meituan|\.waimai\.meituan/) &&
k.setRequestHeader("mtgsig", t.headers.mtgsig),
k.open(o.type, o.url, S, o.username, o.password),
// ... 继续发送请求
});
}
对于 *.waimai.* 域名的请求,签名以 HTTP Header mtgsig 形式附加。对于 open.meituan.com 域名,签名以 URL Query Parameter mtgsig 形式附加。
2.2 H5guard 全局对象
window.H5guard = {
init: // 初始化 SDK
getfp: // 获取设备指纹 → 返回 base64 编码的指纹数据
getId: // 获取设备 ID
initWithKey: // 使用密钥初始化
sign: // 【核心】对请求签名,返回 Promise
xhrResHandle: // XHR 响应处理
fetchResHandle: // Fetch 响应处理
addCommonParams: // 向 URL 添加公共参数
getSGRandom: // 获取随机数 → 返回 32 位 hex 字符串
}
2.3 mtgsig 结构解析
调用 H5guard.sign({url, type, data}) 返回:
{
"url": "https://i.waimai.meituan.com/test/api",
"type": "GET",
"headers": {
"mtgsig": "{...}" // JSON 字符串 (作为 header),或 URL-encoded (作为 query param)
}
}
mtgsig 内部 JSON 结构:
{
"a1": "1.2",
"a2": 1778780005745,
"a3": "55652zwv1u7x5200z5xuu9yw58z3588280v722uxy0w979580v345z16",
"a5": "Y7s6hL/GrkWXLe5g5Lr0/0eHPYXecpdJmVf0z0+TOhkT5cnppN7One/PE5kDKLtu8fMvGvO2txYDwwPXNMlXl1XEGQtLcxtOxFzlp5cYqmDcbDT4WWX3xaPE/7c=",
"a6": "h1.9ui8Cof1dao7hUufqz5zz531irHScaljh7BO4WaVLHltE9lODhQ5218RB2awHqz7vvlQxCSjN+qRCBvua2IJT8sHnPM6/6lWIPpQ1ilwaJIeeXtHABzTRf6KfUDoijM0r2kpGdK8TnUS3d4c07c/HJAMIdswkkapqfzbTiZTzWjB5Ahh+58R9m21COSJnaRKESMK+O4Hx1m0vHHUPZLqmi9X6AXaY9FXyRZhA0pCoy7UzykDNq+bHOro8umcU7rVeiwrn8A63ed9zjI6wabTk+96NthVX6lMSWZexZo1BI/hjuwcV9/+VqwZEjp2FnAMX0Ps1RWmjyGRnzBaTcHYMlAirno2WBgUMA+7wG4hVMzsyQ+FRRu2A26BtxePtcJR7x8sql9grk3DShN4t52Do75ebLTD0sxppQoLFyMGpoztpwULeGnioQOlSGJ+7mxbkDB7XXnjVdfZdx8q90dYYbQ==",
"a8": "83ff90b83e2aab091d3726a131c52067",
"a9": "4.2.0,7,250",
"a10": "8c",
"x0": 4,
"d1": "61ab3d654604ea209e2141f727afd756"
}
2.4 字段含义总结
| 字段 | 类型 | 含义 | 变化规律 |
|---|---|---|---|
| `a1` | String | mtgsig 协议版本号 | 固定 “1.2” |
| `a2` | Number | 毫秒时间戳 | 每次请求更新 |
| `a3` | String | 设备指纹 ID (dfpId) | 会话级固定 |
| `a5` | String | **核心签名** (Base64) | 每次请求重新计算 |
| `a6` | String | H5guard Signature v1 (hs1) | **设备+时间窗口绑定**,约几分钟轮换 |
| `a8` | String | MD5 哈希 (32 hex) | 每次请求更新 |
| `a9` | String | SDK 版本信息 | 固定 “4.2.0,7,250” |
| `a10` | String | 状态码 | 固定 “8c” |
| `x0` | Number | 签名协议版本标识 | 固定 4 |
| `d1` | String | MD5 哈希 (32 hex) | 每次请求更新 |
2.5 关键实验结果
通过三组对比实验验证签名参数变化规律:
实验 1: 相同 URL,连续三次调用
– a2 (时间戳) 递增
– a5 (核心签名) 每次都不同 → 包含时间戳参与计算
– a6 (hs1) 完全相同 → 与具体 URL 无关
– a8, d1 (MD5) 每次都不同
实验 2: 不同 URL,相同时间窗口
– a6 (hs1) 不变 → 确认是设备+时间窗口签名
– a5 (核心签名) 随 URL 变化
实验 3: POST 请求带 Body
– a5 (核心签名) 变化 → POST body 参与签名计算
– a8, d1 也随 body 变化
结论: a5 是由 (URL + 时间戳 + 请求方法 + Body + 设备指纹) 经加密/签名算法生成;a6 是设备+时间窗口的静态签名。
3. 公共请求参数
H5guard SDK 在请求截获时自动向 URL 添加公共参数:
| 参数 | 示例值 | 说明 |
|---|---|---|
| `yodaReady` | `h5` | Yoda SDK 就绪状态 |
| `csecplatform` | `4` | 客户端安全平台标识 |
| `csecversion` | `4.2.0` | 安全 SDK 版本 |
| `_` | `1778779790056` | 毫秒时间戳 (防缓存) |
| `uuid` | `19e277fca29c8-…` | 设备 UUID |
| `dfpId` | `55652zwv1u7x5200…` | 设备指纹 ID (也作为 mtgsig.a3) |
4. API 端点枚举
通过网络请求监控,发现以下 API 端点:
4.1 首页数据 (`i.waimai.meituan.com`)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/openh5/homepage/dsp/resource` | DSP 广告资源 |
| POST | `/tsp/open/openh5/home/shopList` | **商家列表** (需完整签名) |
| POST | `/tsp/open/openh5/home/rcmd` | 推荐商家 |
| GET | `/tsp/open/openh5/set/info` | 集合/标签信息 |
4.2 用户相关 (`i.waimai.meituan.com`)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/openh5/address/list` | 用户地址列表 |
| POST | `/openh5/order/manager/v3/myuncompleteorder` | 未完成订单 |
4.3 用户中心 (`open.meituan.com`)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/user/privacy/get-switches` | 隐私开关配置 |
4.4 安全与风控
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `portal-portm.meituan.com/horn/v1/modules/H5guard_BaseSec/prod` | H5guard 基础安全配置 |
| GET | `portal-portm.meituan.com/horn/v1/modules/H5guardTrack/prod` | H5guard 追踪配置 |
| POST | `msp.meituan.com/fingerprint/v1/notapp/bio/info/report` | 设备指纹生物信息上报 |
4.5 监控与日志
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `catfront.dianping.com/api/pv` | 页面浏览统计 |
| POST | `catfront.dianping.com/api/metric` | 性能指标 |
| POST | `catfront.dianping.com/api/log` | 错误日志 |
| POST | `catfront.dianping.com/batch` | 批量上报 |
| POST | `prism-report-web.dreport.meituan.net` | 前端监控 |
4.6 LX 用户行为追踪
| 域名 | 说明 |
|---|---|
| `lx.meituan.net`, `lx1.meituan.net`, `lx2.meituan.net` | 用户行为数据上报 |
5. 请求头要求
# i.waimai.meituan.com / open.meituan.com
headers = {
"Referer": "https://h5.waimai.meituan.com/",
"User-Agent": "Mozilla/5.0 ...",
"mtgsig": "{...}", # JSON 格式签名 (仅 *.waimai.* 域名)
"Cookie": "", # 可选,影响访问权限
}
# URL Query Parameters (所有 API)
params = {
"yodaReady": "h5",
"csecplatform": "4",
"csecversion": "4.2.0",
"mtgsig": "{...}", # URL-encoded JSON (open.meituan.com)
"uuid": "...",
}
6. 商家列表 403 分析 — 【2026-05-15 深度调查】
shopList 接口返回 HTTP 403,与其他接口 (200 OK) 形成对比:
– dsp/resource (POST) → 200 ✓
– rcmd (POST) → 200 ✓
– set/info (GET) → 200 ✓ (但返回 code: "ILLEGAL_USER_ID")
– address/list (POST) → 200 ✓
– home/shopList (POST) → 403 ✗
– channel/shopList (POST) → 403 ✗
6.1 深入调查过程
为定位 403 根因,进行了系统性排查:
| 尝试方案 | 结果 | 说明 |
|---|---|---|
| 登录后调用 | 403 | userId=394265134,Cookie 完整 |
| 伪造移动端 UA | 403 | `Linux; Android 13; Pixel 7` |
| 覆写 navigator.platform | 403 | `Linux armv8l`, `maxTouchPoints=5` |
| 添加 `_token` 参数 (from rcmd) | 403 | rcmd 的 _token 无法复用 |
| 修改坐标到用户真实地址 | 403 | 贵州坐标 vs 北京坐标均失败 |
| 添加 dsp/resource 的额外参数 | 403 | `wm_ctype`, `wm_appversion` 等无效 |
| 搜索替代端点 (shopListV2/poi/shopList) | 不存在 | API gateway 返回 “no matched api config” |
6.2 关键证据
网关层拦截特征:
– 响应: HTTP 403 Forbidden from openresty (nginx)
– 响应头: x-forbid-reason: . (仅一个点号)
– 响应体: nginx 标准 403 HTML 页面
– 与其他端点的 nginx 直接放行形成鲜明对比
OWL 设备指纹缺失:
– 在浏览器环境中 window.OWL 未定义
– 页面加载了 OWL SDK (owl_1.10.1.js) 但未初始化
– 移动端 Navigator 属性覆写无法触发 OWL 初始化逻辑
common.js 请求增强流程 (逆向分析):
// common.0a2863a5.js — 所有请求统一经过此增强流程:
e.data.wmUuidDeregistration = parseInt(cookie("uuidCanceled") || 0);
e.data.wmUserIdDeregistration = parseInt(cookie("userIdCanceled") || 0);
e.data.openh5_uuid = getUUID();
e.data.uuid = getUUID();
// rcmd 额外添加 _token:
if (e.appendMTSI) {
e.data._token = generateMTSToken(e.url, e.data);
}
// 所有请求统一注入:
e.data = Object.assign({optimus_code: 10, optimus_risk_level: 71}, e.data);
shopList 请求体 (来自 home.0c4f4a88.js):
// requestShopList() 方法逆向:
data: {
pageSize: 20,
page_index: this.pageIndex,
offset: this.pageSize * this.pageIndex,
content_personalized_switch: this.rcmdStatus,
...this.chooseFilterData, // {sort_type:"", slider_select_data:"", activity_filter_codes:""}
...getEncryptLatLng() // 加密坐标 → {optimus_code, optimus_risk_level}
}
6.3 根因分析
403 由 openresty WAF 网关层 直接拦截,而非应用层拒绝。x-forbid-reason: . (点号) 暗示 WAF 规则以暗号形式标记。最可能的原因:
1. TLS/JA3 指纹检测 — Chrome CDP 控制环境与真实移动端 TLS 握手特征不同
2. OWL 设备指纹完整性 — 缺失 WebGL/Canvas/Audio 等多维特征,WAF 判定为非真实设备
3. IP 风控 — 当前 IP 可能在美团风控系统中风险评分较高
4. set_name/region_id 不匹配 — 用户地址在贵州(26.58°N, 106.72°E),但 sessionStorage 中 set_name=waimai-west, region_id=1000520100 指向北京区域
5. 非移动端环境 — 即使伪造 UA 和 navigator,Chrome Desktop 的底层行为(如 WebGL 渲染器、字体列表等)与真机不同
6.4 结论
shopList 端点不可用于 H5 Web 端数据采集。 该端点需要真实的移动设备环境(完整的 TLS 指纹 + OWL 设备指纹 + Native SDK 能力),Web 浏览器无论怎样伪装都无法通过 WAF 检测。
可行的替代数据源:
– rcmd API → 分类配置(金刚位)、筛选器组
– dsp/resource API → 广告/Banner 数据
– address/list API → 用户配送地址
6.5 最新验证 (2026-05-15) — 两个方向同时尝试
基于新发现继续验证了两个方向:补全参数 和 移动端 UA 注入。
方向1: 参数补全 — dsp/resource 完整参数
逆向 home.js 中 dsp/resource 调用的 V 对象:
// home.0c4f4a88.js — dsp/resource 请求数据对象
var V = {
wm_ctype: "openapi",
wm_appversion: "4.0.0",
wm_order_channel: j.get("wm_order_channel") || "",
userId: j.get("userId") || "",
uuid: j.get("uuid") || j.get("iuuid") || "",
userToken: j.get("token") || j.get("mt_c_token") || "",
utm_term: "0", utm_campaign: "0", utm_medium: "0", utm_source: "0",
wm_dtype: "0", wm_dversion: "0", wm_visited: "0",
req_time: (new Date).getTime()
};
dsp/resource 完整请求体 (V + common.js 请求增强):
optimus_code=10&optimus_risk_level=71
&wm_ctype=openapi&wm_appversion=4.0.0&wm_order_channel=default
&userId=394265134&uuid=<UUID>&userToken=
&utm_term=0&utm_campaign=0&utm_medium=0&utm_source=0
&wm_dtype=0&wm_dversion=0&wm_visited=0&req_time=<ts>
&wm_latitude=<lat>&wm_longitude=<lng>
&wm_actual_latitude=<lat>&wm_actual_longitude=<lng>
&wmUuidDeregistration=0&wmUserIdDeregistration=0&openh5_uuid=<UUID>
dsp/resource 返回 {"code":0,"msg":"成功"} 但仅包含 Banner/弹窗/浮标 配置,不包含商家列表数据。
方向2: 移动端 UA — 平台检测变化
注入移动端 UA 前后对比:
| 检测项 | 注入前 | 注入后 |
|---|---|---|
| `navigator.userAgent` | Chrome Desktop | `Linux; Android 13; Pixel 7` |
| `navigator.platform` | `Win32` | `Linux armv8l` |
| `navigator.maxTouchPoints` | 0 | 5 |
| Rohr `deviceModel` | `Win32` | `Linux armv8l` |
平台伪装在 JavaScript 层面 已生效,但 403 依然存在。
方向3: 关键验证 — 签名中 body 数据的哈希
H5guard.sign() 将 body 数据内容哈希到签名字段 a5 中,因此 body 格式必须与签名时一致:
data: 'optimus_code=10&wm_ctype=openapi...' → a5: oIwJTmq5QjoK...
data: '{"optimus_code":"10","wm_ctype":"openapi"...}' → a5: B5ewPxuwZiRG...
data: null/undefined → a5: TKX7pm1OE0zD...
这说明 mtgsig 不仅仅是 URL 签名,而是 URL + Method + Body 的联合签名。在 Python 脚本中必须先将 body 字典 URL-编码,再传给 H5guard.sign(),否则签名验证失败。
方向4: 终局验证 — 从页面自身 JS 上下文调用
为彻底排除参数问题,在浏览器页面自身 JavaScript 上下文中直接调用 H5guard.sign() + fetch():
// 从页面自身调用 shopList — 所有参数与页面本身一致
const body = 'optimus_code=10&...wm_latitude=<lat>...';
const signed = await H5guard.sign({url: shopListUrl, type: 'POST', data: body});
const resp = await fetch(signed.url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'mtgsig': signed.headers.mtgsig},
body: signed.data
});
// → HTTP 403, x-forbid-reason: .
即使从页面自身执行,shopList 仍然返回 403。这确凿证明:
WAF 网关对 `/tsp/open/openh5/home/shopList` 的拦截是**路径级别的**,与参数完整性、签名正确性、UA 伪装均无关。
对比: dsp/resource 从页面自身调用
const body = 'optimus_code=10&wm_ctype=openapi&...wm_actual_latitude=...';
const signed = await H5guard.sign({url: dspUrl, type: 'POST', data: body});
// → HTTP 200, {"code":0,"data":{...banner...}}
补充发现: common.js LocationWhiteUrl
从 common.0a2863a5.js 逆向出坐标注入白名单 (27 个端点):
LocationWhiteUrl = [
"openh5/homepage/dsp/resource", ← dsp/resource 在列表中
"openh5/poi/filterconditions",
"openh5/channel/kingkongshoplist",
"openh5/address/list",
"openh5/homepage/kingkong",
"openh5/homepage/poilist",
"openapi/v1/poi/food",
"openh5/poi/comments",
"openh5/order/v2/submit",
"openh5/order/v2/preview",
... 共 27 个端点
]
shopList (/tsp/open/openh5/home/shopList) 不在白名单中 — home.js 直接调用 getEncryptLatLng() 加密坐标后内联到请求体。
6.6 终局对比
| 端点 | mtgsig 位置 | WAF 状态 | 返回数据 | 备注 |
|---|---|---|---|---|
| dsp/resource | HTTP Header | 200 (带 mt-gateway-error:true) | Banner/弹窗配置 | 工作正常 |
| rcmd | HTTP Header | 200 | 分类/金刚位/筛选器 | 工作正常 |
| address/list | HTTP Header | 200 | 用户地址列表 | 工作正常 |
| **shopList** | HTTP Header | **403 (x-forbid-reason:.)** | — | **路径级 WAF 拦截** |
| shopListV2 | HTTP Header | 200 (body: “no matched api”) | 空 | 接口未实现 |
| poi/shopList | HTTP Header | 200 (body: “no matched api”) | 空 | 接口未实现 |
shopList 端点不可用于 H5 Web 端数据采集。 该端点需要真实的移动设备环境(完整的 TLS 指纹 + OWL 设备指纹 + Native SDK 能力),Web 浏览器无论怎样伪装都无法通过 WAF 检测。
可行的替代数据源:
– rcmd API → 分类配置(金刚位)、筛选器组
– dsp/resource API → 广告/Banner 数据
– address/list API → 用户配送地址
7. Yoda Seed SDK —— 风险验证流程
Yoda Seed (yoda.seed.js, v1.6.9) 负责加载人机验证模块:
页面加载 → Yoda Seed init()
├── 检查 KNB (美团 Native SDK) 环境
│ └── KNB.ready() → 获取设备信息
├── GET {verify_host}/v2/ext_api/page_data
│ └── 请求参数: requestCode, feVersion, source, layer
├── filterRiskLevel() → 遍历风险等级列表
│ └── 匹配设备平台 (iOS/Android/Web/MiniProgram)
├── loadSource() → 动态加载验证 JS/CSS
└── moduleInit() → 初始化验证模块 (滑块/点选/短信等)
风险等级 (riskLevel) 路由:
| riskLevel | 验证方式 | 说明 |
|---|---|---|
| 1-70 | 低风险 | 无需验证 |
| 71-105 | 中风险 | 滑块/点选验证 |
| 106-208 | 高风险 | 需 KNB Native 验证 |
| 200 | 特殊 | 小程序环境 |
8. 安全架构总结
┌─────────────────────────────────────────────────────────┐
│ h5.waimai.meituan.com │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 业务层 (home / index / common) │ │
│ │ ├── jQuery AJAX 全局拦截器 │ │
│ │ └── 调用 H5guard.sign() 签名每个请求 │ │
│ └──────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼────────────────────────────────┐ │
│ │ H5guard.js (343 KB) — 请求签名与拦截 │ │
│ │ ├── XMLHttpRequest.prototype 劫持 │ │
│ │ ├── fetch() 劫持 │ │
│ │ ├── mtgsig 签名生成 (a1-a10, x0, d1) │ │
│ │ ├── 公共参数注入 (yodaReady, csecplatform, ...) │ │
│ │ └── 设备指纹采集 (getfp) │ │
│ └──────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼────────────────────────────────┐ │
│ │ 辅助安全层 │ │
│ │ ├── OWL — 设备指纹 + 生物特征采集 │ │
│ │ ├── Yoda Seed — 人机验证 (滑块/点选/短信) │ │
│ │ ├── Rohr — 业务数据监控上报 │ │
│ │ ├── LX — 用户行为追踪 │ │
│ │ └── Logan — 客户端日志 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
关键安全机制
1. 双层签名: mtgsig (a5 核心签名) + hs1 (a6 设备签名),双重验证
2. 全量劫持: 劫持 XHR/Fetch 原型,无法绕过签名
3. 时间绑定: a6 随时间窗口轮换,防止重放
4. 设备绑定: a3 (dfpId) 和 a6 都绑定设备指纹
5. 多层风控: 商家列表 403 表明存在分层的接口访问控制
9. 技术总结
9.1 攻击面分析
– mtgsig 签名算法: 实现在 343KB 的 H5guard.js 中,高度混淆,核心签名算法通过原生加密 API (SubtleCrypto) 实现,难以纯静态分析
– 设备指纹: OWL + H5guard 双重采集,反模拟能力强
– Yoda 验证: 动态加载验证模块,未通过验证的请求被拦截
– 时间窗口: a6 签名定期轮换,限制了重放攻击窗口
9.2 复现难度
| 组件 | 难度 | 说明 |
|---|---|---|
| mtgsig 静态分析 | 高 | 343KB 混淆代码,内联加密逻辑 |
| mtgsig 运行时调用 | 低 | `H5guard.sign()` 可直接在浏览器控制台调用 |
| 设备指纹 (OWL) | 高 | 采集 WebGL/Canvas/Audio 等多维特征 |
| Yoda 验证 | 高 | 服务端动态下发验证策略 |
9.3 安全建议 (给美团)
– mtgsig 签名建议加入 URL path 级别的绑定,防止跨接口复用
– H5guard.sign() 应在代码中增加 console.log / debugger 检测
– 设备指纹采集应增加环境一致性校验(如 User-Agent vs Navigator 属性匹配)
9.4 脚本运行注意事项
Chrome 启动命令 (Chrome 130+ 必须加 --remote-allow-origins):
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--remote-debugging-port=9222 ^
--remote-allow-origins=* ^
--user-data-dir="C:\temp\chrome_debug"
CDP WebSocket 连接修复: Python websocket-client 库默认发送 Origin: http://localhost:9222,Chrome 130+ 会拒绝此自引用 Origin。通过 suppress_origin=True 跳过 Origin 头即可。MCP (puppeteer) 不存在此问题因为不发送同源 Origin。
采集脚本返回值: 当 shopList 被 403 阻塞时,collect() 返回 dict 中混合了 category 数据和 _rcmd_config / _note 等元数据键。CSV 写入和摘要打印需过滤下划线开头的键。
9.5 最终能力评估
| 端点 | 状态 | 返回数据 |
|---|---|---|
| rcmd | ✅ 200 | 分类金刚位、筛选器配置 |
| dsp/resource | ✅ 200 | Banner/弹窗/浮标配置 |
| address/list | ✅ 200 | 用户配送地址列表 |
| shopList | ❌ 403 | WAF 路径级拦截 |
| set/info | ⚠️ 200 (ILLEGAL_USER_ID) | 需合法账号绑定 |
附录 A: mtgsig 调用示例 (浏览器控制台)
// 在美团外卖 H5 页面控制台执行 — 注意 H5guard.sign() 是 async 函数
const result = await H5guard.sign({
url: 'https://i.waimai.meituan.com/openh5/homepage/dsp/resource',
type: 'POST',
data: 'optimus_code=10&wm_ctype=openapi' // body 必须 URL-编码
});
console.log(JSON.parse(result.headers.mtgsig));
// → {a1: "1.2", a2: 1778780005745, a3: "...", a5: "...", a6: "h1...", ...}
附录 B: CDP 连接最小示例 (Python)
from websocket import create_connection
import json, urllib.request
# 1. 获取 WebSocket URL
resp = urllib.request.urlopen("http://localhost:9222/json")
pages = json.loads(resp.read())
ws_url = pages[0]["webSocketDebuggerUrl"]
# 2. 连接 (Chrome 130+ 必须 suppress_origin)
ws = create_connection(ws_url, timeout=30, suppress_origin=True)
# 3. 执行 JavaScript
msg = json.dumps({
"id": 1, "method": "Runtime.evaluate",
"params": {"expression": "document.title", "returnByValue": True}
})
ws.send(msg)
print(json.loads(ws.recv())) # → {id:1, result:{result:{value:"美团外卖"}}}
ws.close()
完整仓库见 mtgsig_demo.py —— 包含签名调用、多分类采集、WAF 回退的完整实现。







暂无评论内容