☰
东方航空m端航班价格查询逆向实录:从抓包到签名还原与数据采集
2026/9/30 8:31:30 网站建设 项目流程

做航班价格采集的人大概都头疼过同一个问题:聚合平台的数据好抓,但一手数据永远在航司自己手里。我最近在弄一个低价提醒的小工具,需求很简单——每天查几次特定航线的往返票价,低于心理价位就推一条通知。数据源绕不开东方航空官方渠道,于是就有了这篇m端逆向实操记录。

先交代一下背景。所谓“m端”,就是航空公司的移动端网页,通常挂在m.ceair.com或者类似的子域名下,面向手机浏览器访问。和完整的App相比,m端的代码量小、更新频率相对可控,而且本质上还是一个Web页面,前端逆向技术的通用手段基本都能用上。相比之下,东方航空App的逆向就要麻烦不少,涉及安卓逆向那一套流程,得处理加固壳、so层逻辑、Frida Hook等等,对只是想拿个航班价格的人来说,投入产出比太低了。

这篇内容我按自己实际走过的路径来写:从接口定位、加密参数分析、签名算法还原,到最后怎么把它工程化落地跑稳定。整个过程用的是最常规的浏览器调试器加Node.js补环境方案,不需要太高门槛,也不需要背任何逆向框架文档——说白了一个有前端基础、能看懂XHR请求的人,照着操作一遍就能上手。

1. 为什么盯上东方航空m端,而不是App或开放平台

先说需求层面的判断,很多人一上来就想着去逆向App,我觉得这属于路线选择失误。

航司App的逆向,常规做法是把APK拉下来,用jadx反编译看java层的代码逻辑,然后对着Smali代码加日志、重打包;遇到加固壳还得先脱壳,再把可疑函数挂到Frida上做动态调试。整个过程不是不能做,而是时间成本高。我见过不少朋友在“frida逆向工具比较”“app逆向”“安卓逆向”这些话题下花了很多时间,最后发现目标App只是WebView套壳,核心接口照样走的是一套Web协议——你前面那些脱壳工作全白干。

m端Web逆向就不一样。它跑在浏览器里,所有请求和响应都能被开发者工具完整看到,加密逻辑以JavaScript形式直接下发到前端。这就意味着,只要定位到关键函数,还原整个签名过程是件相对确定的事。再加上m端一般不会做太重的代码保护,顶多压缩混淆一下,跟App那种so层算法完全不是一个难度等级。

当然,也有人会问:为什么不走官方开放平台?我试过,原因很现实:开放平台对个人开发者不友好,申请资质、签约流程一套走下来周期很长,而且开放接口的覆盖范围、频率限制都不一定满足个人工具的需求。对于“每天查三次价格”这种低频场景,从m端拿数据反而更直接。

还有一个容易被忽略的点:m端的请求结构往往和App端是同一套接口网关,只可能在加密参数上略有差异。把m端这条路跑通之后,如果哪天真需要去碰App端,你至少已经把接口语义、数据结构、签名规则的公共部分都摸清了,省钱省力。所以我最终确定了技术路线:Chrome开发者工具定位请求,全局搜索加密参数,断点回溯签名函数,Node.js补环境,最后用Python组装并发请求。

2. 抓包定位航班查询接口:从一条请求开始

逆向的第一步永远是抓包,这一步的目的不是分析加密,而是搞清楚“服务器到底需要哪些参数、返回了什么数据”。

打开东方航空m端首页,找到航班查询入口。不用急着登录,匿名状态一般就能查航班和价格。进入页面之后先把Chrome开发者工具打开,Network面板勾选上Preserve log,防止页面跳转时清空记录,然后在页面上输入出发地、目的地、日期,点击查询。

请求发出去之后,Network面板里会刷出一批XHR或Fetch请求。这时候需要做的事是筛选:先按Fetch/XHR过滤,再去看URL里有没有flight、search、price、schedule这类关键字。通常航班查询的接口URL会比静态资源长很多,而且带着一堆query参数,一眼就能认出来。我这次遇到的接口路径结构大致是这样:请求URL以/searchService或/queryApi开头,方法为POST,请求体是JSON。

找到目标请求之后,右键点击它,选择Copy as cURL。把这个命令粘到终端里跑一次,先验证一件事:直接重放这个请求,服务器会不会正常返回航班数据。

这一步得到的反馈很关键,基本分两种情况:

  • 如果直接返回数据,说明当前这套接口还没有签名校验,这个项目可以简化一大半。
  • 如果返回401、403,或者提示sign error、invalid token,那就说明请求里的某个参数是动态加密的,我们需要继续往下挖。

我在东航m端遇到的是第二种情况,而且错误信息相当明确,直接提示签名校验失败。这反而好办,因为错误类型已经告诉你了:问题出在某个需要动态计算的参数上。

接下来看请求体里的字段。一般分成两类:

  • 业务明文参数:出发城市、到达城市、日期、航程类型、乘客数、舱位代码。这些是可读的,直接暴露在JSON里。
  • 加密动态参数:大部分情况下会有一个sign或者signature字段,也有一些接口会放在特定的header里,比如x-sign、x-token。偶尔还伴随一个timestamp或nonce。

把请求体重放时,可以手动改几个字段来验证哪些参数影响签名。比如只改日期,保留原来的sign,重放一次,如果报错,说明签名和业务参数是绑定的;如果正常返回,说明参数没参与计算。别小看这一步,它能帮你把判断范围缩小很多。

如果你观察得够细,还会发现timestamp这个字段每次点击查询都不一样。这种情况下,签名大概率把时间戳也吃进去了,用来防止重放攻击。这就是为什么直接复制cURL重放会失败——时间戳早过期了。

3. 顺着sign参数找到加密函数:断点与调用栈实战

定位到sign参数之后,真正的逆向分析才开始。这里我说的“逆向”,其实就是顺着JS代码反推出签名函数的输入、处理和输出。很多教程喜欢直接说“搜索参数名”,但实际操作中光靠搜索往往一头雾水,因为压缩后的JS文件里带sign关键字的地方可能有几十处,根本分不清哪个是我们要的。

我习惯优先看Initiator面板。在Network面板点开目标请求,右侧会有Initiator一栏,点进去能看到发起该请求的JS调用栈。这个调用栈是浏览器自动记录的,直接从栈顶往下一层一层点,就能看到请求是在哪个函数里fetch或xhr.send的,然后在附近找sign的赋值语句。大多数情况下,签名函数的调用点就在请求构造函数上方两三行,属于“近水楼台”的位置。

如果你用的调试器版本比较老,Initiator信息不够清晰,那就用笨办法:在Sources面板里按Ctrl+Shift+F全局搜索sign字段名。搜索结果会有很多,怎么筛?我的经验是优先找符合这三个条件的文件:

  1. 文件名带search、query、flight、config相关字样。
  2. 文件体积不大,几十KB到两三百KB之间,太大的一般是公共库,太小的一般是挂载脚本。
  3. 文件里有sign和请求URL出现的上下文关联。

定位到疑似赋值点之后,在那个JS文件里找到签名函数的定义,然后直接下断点。重新在页面上触发一次查询,断点命中后,看Scope面板,观察函数入参;再开Call Stack面板,一点一点往外层回溯。这一步基本就能看到整个签名的生成链路了。

下面这个示例只是签名函数的一种常见形态,我用它来说明套路,不是东航的真实代码。搞清楚结构,比抄到现成代码重要得多:

function generateSign(params) { var keys = Object.keys(params).sort(); var raw = keys.map(function(k) { return k + '=' + params[k]; }).join('&'); var time = new Date().getTime(); var finalStr = raw + '&timestamp=' + time + '&salt=你的盐值'; return md5(finalStr).hexdigest(); }

这类函数拆开来看,核心操作无非四件事:参数排序、拼接字符串、加固定盐值、算摘要。算法本身不复杂,大部分航司类Web站点的签名都逃不出这个框架。困难往往在细节上,比如参数排序规则是字典序还是根据接口文档自定义序、盐值是写死的还是在代码里动态生成、摘要算法是MD5还是SHA256、是否先做了HMAC再做十六进制编码——这些信息只能靠断点来验证,不能拍脑袋猜。

有一个坑我要特别提醒:不要看到timestamp就以为它是签名密钥。很多接口里时间戳只是作为防重放参数存在,和签名计算无关。判断方法很简单,把时间戳字段从请求体里去掉,用同一个签名重放,如果服务器不报错,说明时间戳没参与计算;如果报错,再把它拼进签名逻辑里做测试。

另外,搜索JS代码时要注意压缩混淆的情况。生产环境的JS几乎都是压缩过的,变量名可能叫t、e、n,看着很劝退。我的对策是先找人名化程度高的文件——有些模块只是做了uglify压缩但保留了函数名,有些甚至保留了注释,从这些文件入手会顺利很多。如果你运气不好,所有文件和函数名全是混淆过的,那就在断点命中后多花点时间看Call Stack,把整个调用链的输入输出走一遍,一样能还原逻辑,只是时间会翻倍。

顺带说一句,断点调试这个阶段,最好把浏览器自带的Overrides功能用起来。把目标JS文件保存到本地,结合本地替换,可以自由地在签名函数里加日志、改逻辑,不用每次刷新页面都重新梳理一遍压缩代码。这是一个能让效率翻倍的操作,建议尽早学会。

4. 把签名算法搬进本地运行:补环境与结果验证

定位到签名算法之后,接下来的目标是脱离浏览器环境,在我们自己的代码里把签名算出来。这一步最常见的做法,就是把签名相关的那段JavaScript代码原封不动地拿到Node.js里运行,然后由Python或Node主程序负责发起HTTP请求。

但这里有一个问题:这段JS是写在浏览器环境里的,运行时可能依赖很多浏览器对象,比如window、document、navigator、localStorage。Node.js默认只有V8引擎,不提供这些全局对象,直接node运行会报各种xxx is not defined错误。补环境就是针对这些缺失的全局变量,一个一个补上去,让这段JS以为自己在浏览器里运行。

我的做法是这样的:

第一步,把前面定位到的签名函数以及它依赖的工具函数、常量定义,从源JS文件里完整拷贝出来,单独存成一个sign_core.js。

第二步,在Node项目里建一个入口文件,先声明缺失的全局变量,再require或eval这段代码。补环境时有一个优先级讲究,先补最常见的三个:window、navigator、document。代码类似这样:

global.window = {}; global.window.navigator = { userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) ...', platform: 'iPhone', language: 'zh-CN' }; global.navigator = global.window.navigator; global.document = {}; global.document.cookie = '';

第三步,调用签名函数,传一组测试参数进去,把输出结果记下来。

第四步,回到浏览器里,在DevTools的Console里手动调用原始页面的签名函数,同样调用一组参数,对比两边的输出是否一致。

前后一致,说明补环境基本到位;不一致,很可能是某个全局变量或者随机数生成逻辑没补全。这里有一个很容易被忽略的检查点:有些JS代码会检测函数是否被Hook过,常见手段是调用函数的toString()看源码是否和预期一致。如果你在调试阶段给页面里的签名函数打过补丁,浏览器环境里的函数源码其实已经被你修改过了,对比结果会有误导性。所以验证的时候,尽量在全新页面上操作,别用改过的文件做基准。

补环境过程中还有一个高频报错是小写的window有了,但window.a内部的某个属性缺失,比如window.location.href、window.screen.width。这些属性报错时,根据报错信息补上就行。另一个高频坑是代码里用了document.createElement('div')或document.getElementById,这种DOM操作在补环境阶段很难完全模拟。我的经验是——如果签名函数本身不需要DOM操作,就不要把整段包含DOM逻辑的代码拷进来,尽量只保留签名计算路径上的函数和常量。抠函数的过程要克制,不要贪多。

Node里跑起来之后,有人可能还会问:能不能用Frida直接Hook?Frida确实是动态调试的神器,尤其是安卓逆向和App逆向场景,市面上关于“frida逆向工具比较”“app逆向”的讨论也很多。但对m端Web项目来说,用Frida反而绕远了——浏览器本身就是完美的调试环境,补环境也很轻量。除非你面对的是某个嵌在原生App里的WebView页面,需要直接在真机上Hook,那才轮到Frida上场。

补环境跑通之后,签名算法这块就算彻底拿下了。接下来要解决的是工程化落地的问题。

5. 工程化落地:从“算出签名”到“稳定取数”要过的几道坎

能算出签名只是第一步,离“稳定取数”还有很长一段路。这个阶段踩的坑,比逆向阶段多得多。我把实际遇到过的问题按出现频率排队,一个一个说。

第一道坎是时间戳对齐。签名里如果包含时间戳,而本地时间和服务器时间差得太多,请求大概率会被拒绝。我在本地测试时遇到过一种情况:Windows系统时间慢了30秒,签名按本地时间生成,服务器直接返回403。解决办法是在代码启动时先做一次时间校准——请求东航服务端的某个公开接口,读取响应头里的Date字段,计算出本地时间和服务器时间的偏移量,后续生成签名时统一加偏移量。这个做法对任何带有时间戳签名的接口都通用,建议直接封装成工具函数。

第二道坎是Cookie与会话管理。m端的某些接口,匿名状态下就能访问,但要求请求头里携带当前页面的Cookie,尤其是route、sessionId这类维持会话的参数。Cookie过期了,签名再对也会被拒。我的处理方式是把Cookie维护成一个独立的模块:第一次访问首页拿到初始Cookie,然后一直复用;如果收到特定错误码,自动重新获取Cookie再重试。有些功能走会员价时Cookie里还需要保持登录态,那就更复杂一点,登录后的token和Cookie要定期刷新,不能写死。

第三道坎是频率限制和风控。不要小看这个,航司的接口风控比普通小网站聪明得多。刚开始测试时,我写了个循环脚本一口气跑了30次查询,结果第15次左右返回了一个异常页面,提示需要滑块验证。遇到这种情况,单纯换IP不是好办法,更务实的做法是控制请求节奏:单次查询间隔至少2秒,单会话并发数控制在3以内,UA随机切换,必要时在两次查询之间模拟一次正常的页面访问,让服务端觉得这是一个真人在使用。

第四道坎是响应数据可能也是加密的。有些接口返回的JSON是明文,但东航m端的部分接口,返回的数据不是直接的业务字段,而是一段加密的字符串,需要前端JS做解密才能看到航班列表。处理方式也很直接:和签名函数一样,在Sources里搜索解密函数的关键特征,比如decrypt、AES、RSA、CryptoJS,找到之后把解密逻辑同样抽出来,在本地补环境里跑。解密用的密钥一般在页面JS里写死,或者通过另一个接口下发,找到它的位置,剩下的就是常规的密码学操作还原了。

第五道坎是异常分类。经过一段时间听跑,我总结了一套错误码分类规则,放在代码里做分流处理:签名错误单独一类,一般是代码改动或算法变了;Cookie失效单独一类,默认触发重新登录流程;风控拦截单独一类,触发降速处理;参数错误单独一类,说明业务字段拼错了。把这几类错误码分开,能省掉大量排障时间。光凭响应文本去猜状态,容易把问题带偏。

代码结构上,我强烈建议不要把签名算法直接埋在爬虫脚本里,而是独立成一个子模块,通过一个配置接口传入参数、返回签名结果。这样一旦签名算法变了,只需要更新签名模块,主程序基本不用动。我这次是分成四个文件:sign.js管签名计算,request.py管请求发送和重试,config.json管接口地址、参数模板、频率设置,main.py管调度和结果存储。模块清晰的好处不用多说,维护的时候谁用谁知道。

6. 边界、合规与长期维护

逆向技术本身是中性的,但怎么用必须讲清楚。我这次做的低价提醒工具,查询频率极低,一天只有几次,本质上和一个真人用户在手机上查票没有区别,不构成对服务端的压力。这应该成为一个默认的开发底线。

具体来说,有几点自我约束供参考:第一,不做大规模抓取,更不做批量爬取后再转售数据的事情;第二,不碰涉及个人隐私的接口,比如会员信息、实名认证、票号查询这一类,这些接口即使是技术上有办法绕过鉴权,也绝对不能碰;第三,如果查询频率进入每分钟几十次的量级,那就不是个人工具的范畴了,应该直接去联系官方谈合作或走开放平台,而不是继续和风控对抗。

另外还要有长期维护的心理准备。m端页面前端一改版,签名算法就可能跟着变。应对方式是在代码里做一个轻量监控:每次取数成功时,记录当前用到的JS文件哈希值和签名函数的特征注释;如果连续三次请求都报签名错误,大概率是页面改版了,优先去重新抓包分析新的签名逻辑,而不是盲目调参数。

还有一点经验,签名算法找回不来的时候不要立刻从头开始逆向。先去Network面板里抓几个最新的请求,看看请求体里新增了哪些字段,旧的字段是否被删掉了。很多时候前端只是顺手改了一下拼接顺序,或者加了一个新的nonce字段,按增量变化的思路去更新签名逻辑,比全量重审快得多。

最后说一句实在话,m端逆向这类事,最花时间的环节从来不是解密算法本身,而是“定位”。从打开调试器到最终找到签名函数,我第一次做东航这个目标用了一个多小时,其中大半时间都耗在压缩代码里翻找和验证上。一旦走通一次,你就会发现所有航司类m端的套路都极其相似——参数排序、拼串、加盐、摘要,翻来覆去就那几样。自己整理一份逆向Checklist,把搜索参数名、看Initiator、断点回溯、补环境验证这几个标准动作固化下来,之后遇到任何同类目标,都会快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询