1. 项目背景:为什么航班查询接口要上签名机制
1.1 从飞常准的接口设计说起
飞常准是国内比较知名的航班动态查询工具,它的微信小程序提供了实时航班状态、起降时间、延误信息、航站楼登机口等查询能力。这些数据背后是一整套接口体系,而其中最关键的防护层,就是签名机制。
我最初接触这个课题,是在做小程序安全评估的时候。要判断一个接口是否存在越权、重放、批量爬取等风险,绕不开签名机制——它是接口安全的第一道闸门。不夸张地说,签名这关过了,后面几乎畅通无阻;反过来,签名校验做得够扎实,外面的人想碰这些接口,难度会直线上升。
这篇内容不是教你怎么去薅某个平台的航班数据,而是从技术研究的角度,把小程序接口逆向分析的方法论完整梳理一遍。无论你是做小程序开发的工程师、刚入门安全方向的学生,还是单纯对接口防护机制感兴趣的技术爱好者,这套思路和流程都可以直接迁移到你自己的项目、自己的接口测试里。我会把每一步为什么这么做、有哪些坑、怎么验证结果,全部摊开来讲。
1.2 签名机制到底在防什么
要理解签名机制,先得搞清楚它防的是谁。小程序这种场景有个天然特点:客户端代码打包下发到用户手里,是完全可以被反编译的。攻击者根本不需要操作界面,直接照着抓到的请求格式,模拟发送HTTP请求就能调你的接口。
如果没有签名校验,会出什么问题?最常见的是三类:
- 数据被批量爬取——写个循环脚本,几分钟就能把大量航班动态数据搬走
- 请求被恶意重放——抓到一条合法请求反复提交,刷接口、刷业务逻辑
- 参数被篡改——改掉查询参数尝试越权,比如把航班号改掉看别人的数据,或者把日期改掉绕过某些限制
签名机制的核心目标,就是让每个请求都携带一个"只有合法客户端才能算得出来"的值。服务端收到请求后,用同样的算法重新计算签名,能对得上就放行,对不上就拒绝。这样一来,盲目构造的请求包就失去了意义。理解了这层逻辑,你就知道后面所有逆向分析的着力点在哪里了。
2. 小程序接口分析的完整技术路线
2.1 抓包工具选型与基础配置
要分析接口,第一步永远是抓包。微信小程序走的是HTTPS协议,常规抓包工具就能处理,前提是证书装到位。我常用的组合是Charles加手机代理,Fiddler也可以,看个人顺手程度。
这里有个前提要说明:有的小程序做了SSL Pinning(证书绑定),常规代理方案抓不到明文。实测飞常准小程序没有做这层防护,普通抓包就能看到请求内容,这在商业小程序里算是比较常见的状态。碰到有Pinning的目标,得先过掉证书校验才能继续,那是另一个话题,这次先按下不表。
配置抓包环境的基本步骤,我整理了一下:
- 电脑安装Charles或Fiddler,开启SSL Proxying,生成并信任CA证书
- 手机和电脑连同一个局域网,手机手动设置HTTP代理指向电脑IP加端口
- 手机浏览器访问证书下载地址,安装并信任该证书
- 打开微信小程序,随便发起一个查询,观察抓包工具里的流量
有个高概率踩中的坑:安卓7.0以上系统默认不信任用户安装的证书,如果HTTPS请求怎么也抓不到,十有八九就是这个原因。常见解法是把证书装进系统证书目录(需要root),或者用已经配好证书的模拟器来跑。iOS上相对宽松一点,但也要去设置里信任描述文件,这点容易漏。
提示:抓包只是手段,真正的难度在于从海量流量里精准定位到目标请求,再抽丝剥茧还原出背后的计算逻辑。
2.2 流量定位与关键请求识别
打开小程序随便点几下,抓包工具里会瞬间涌入几十上百条请求,静态资源、埋点、广告、业务接口混在一起。我习惯先按类型粗筛一遍:
- 静态资源(图片、CSS、JS文件)——先忽略
- 接口请求(返回JSON数据的)——重点关注
- 埋点和统计请求——暂时忽略,除非后面需要
定位航班查询接口有个小技巧,看URL关键词。飞常准的接口路径里基本会包含flight、schedule、dynamic这类词,一眼就能认出来。找到之后,把请求参数和返回数据放在一起对照,能很快确认哪个接口对应哪个功能。
定位到目标请求后,先别急着分析签名。第一件事是把完整的请求信息记录下来:URL、请求方法、请求头、全部参数、返回内容。最好直接把curl命令从抓包工具里导出保存,这些是后续所有分析工作的事实基础。我之前吃过亏,分析到一半发现初始请求数据被工具自动清理了,又得重新抓一遍,浪费时间。
3. 签名机制的核心拆解
3.1 签名参数的组成结构
大多数小程序的签名请求,参数名基本逃不开sign、signature、token、nonce、timestamp这几类。飞常准的请求里有一个明显的签名字段,同时配套一个时间戳字段,这是非常经典的签名设计模式。
从抓包里看到的一个典型签名请求,长这样(参数名与值已做脱敏处理):
{ "flight_no": "CA1234", "date": "2025-06-15", "timestamp": "1749000000", "nonce": "a1b2c3d4e5f6", "sign": "9f8e7d6c5b4a3f2e1d0c" }timestamp是请求发起时的Unix时间戳,nonce是一次性随机字符串,sign就是整个签名机制的产出物。服务端拿到请求后,会用自己持有的密钥按相同算法重新计算sign,与请求里的值比对,同时检查时间戳是否在容忍窗口内。时间戳过期或者签名对不上,直接拒绝。
那sign到底是怎么算出来的?这才是核心问题。虽然各平台的实现细节不同,但骨架高度一致:参数排序、拼接、加密。搞清楚这套骨架,就等于拿到了解开所有类似机制的钥匙。
3.2 从请求参数到签名字符串
小程序接口的签名算法,本质上是一个"参数排序加拼接加加密"的过程。通用步骤如下:
- 收集所有业务参数,剔除签名本身和空值参数
- 按参数名的字典序(ASCII码顺序)排序
- 用key=value的形式拼接成字符串
- 在拼接结果的首尾或中间插入AppSecret之类的密钥
- 对拼接结果做MD5、SHA256或HMAC计算
- 结果转成十六进制或Base64字符串,得到sign
说起来简单,实际还原的时候全是细节。比如拼接用的连接符到底是&还是空字符,参数之间要不要加分隔符,密钥加在开头还是结尾还是中间,这些都会直接影响最终结果。而且你只有"输入参数"和"最终输出"两个端点,中间链路全靠推断,所以每一步都要小心验证。
这类签名机制的薄弱点在于:密钥必须存在于客户端代码里。只要客户端能正常算出签名,密钥就有办法被提取出来。服务端能做的,只是让提取过程变得困难一些。
3.3 时间戳与nonce的联动校验逻辑
时间戳和nonce这两个字段,很多人会忽略,其实它们大有讲究。
时间戳是为了防重放。服务端拿到timestamp后,会判断它和当前时间的差值,超出容忍窗口直接拒绝。所以你在还原签名算法的时候,时间戳一定要用当前时间,不能写死。用旧时间戳发起请求,服务端一秒钟都不会犹豫,直接返回错误。
nonce是为了防"时间窗口内的重放"。理论上只要时间戳合法,同一秒内重放同一个请求是可能被接受的。加上随机nonce之后,服务端可以把已用过的nonce缓存起来,发现重复就判定异常。设计严谨的系统,nonce还会有过期时间,避免缓存无限膨胀。
在逆向分析时有个值得注意的点:服务端到底校验了哪些参数?不同平台策略不同,有的把所有业务字段全部纳入签名,有的只签关键字段。判断方法很简单——改某个参数的值,看签名是否还通过。改了之后签名失效,说明这个字段参与签名;没变,说明它不参与。这一步的结论,直接决定你后面构造的拼接串对不对。
4. 实操环节:从拿到请求到还原签名逻辑
4.1 定位小程序代码里生成签名的位置
光看不行动不行。要真正还原签名算法,必须找到小程序代码里生成签名的那段逻辑。
微信小程序的代码包是wxapkg格式,里面是编译后的JavaScript代码。拿到这个文件有几条常见途径:从手机微信缓存目录提取(需要root或备份能力)、通过脱壳工具从调试环境提取、或者从PC端微信缓存目录提取。拿到之后用解包工具拆开,就能看到一堆JS文件。
这些JS文件通常经过了压缩和混淆,但这不影响搜索。在解包后的JS文件里搜sign、md5、sha256、sort、timestamp这些关键词,通常能快速锁定可疑代码段。我自己的经验是先从sign这个词搜起,命中率极高——不管混淆得多厉害,签名结果最终要赋值给名为sign的参数,这个映射关系绕不开。如果代码里把签名参数别名改成了s或sig,那就多花点时间,去请求构造的地方找线索。
4.2 还原加密流程的通用思路
找到关键代码后,如果代码没有重度混淆,一眼就能看出算法结构。但现实往往是代码被压缩成一行,变量名全是a、b、c,逻辑链直接被拉平。我的处理步骤一般是:
- 先用js-beautify或prettier把代码格式化
- 在格式化后的代码里找关键函数,插入日志输出中间变量
- 用Node.js搭一个本地环境,把可疑函数单独拎出来执行
- 输入抓包拿到的真实参数,看输出是否与抓包结果吻合
这里强烈建议搭一个本地的Node.js调试环境。很多小程序代码跑在微信特有的运行环境里,直接运行会报环境缺失错误。我的做法是:把签名相关函数抽取出来后,用webpack打包,缺失的全局变量用mock补上,在Node里跑通。跑通之后做一次"交叉验证"——输入真实请求的全部参数,算出的签名如果和抓包值一致,说明还原对了;不一致,说明还有细节没补全。
交叉验证是整个逆向过程中最有成就感的一步,也是最关键的一步。它意味着你不只是猜了个大概,而是真正理解了算法的完整链路。
4.3 用交叉验证确认签名算法
假设某个接口的签名逻辑是:参数按字典序排序拼接,尾部追加密钥,再做MD5,那么验证脚本的核心逻辑大致是这样的:
const crypto = require('crypto'); function generateSign(params, secret) { const keys = Object.keys(params).sort(); let base = ''; keys.forEach(key => { // 跳过空值和签名相关字段 if (params[key] !== '' && key !== 'sign' && key !== 'nonce') { base += `${key}=${params[key]}&`; } }); base = base.slice(0, -1); // 去掉末尾多余的& base += secret; // 尾部拼上密钥 return crypto.createHash('md5').update(base).digest('hex'); } // 用抓包到的参数做验证 const sampleParams = { flight_no: 'CA1234', date: '2025-06-15', timestamp: '1749000000' }; const sign = generateSign(sampleParams, 'your_secret_placeholder'); console.log(sign);如果输出的值跟抓包里的sign一致,签名机制的基本逻辑就梳理清楚了。不一致也正常,可能是过滤规则不同、排序方式不同、密钥藏在别处,或者加密算法不是MD5而是别的。继续用控制变量法逐项排查就好。
注意:上面这段代码是研究签名算法通用原理的示例,不代表任何具体平台的真实实现。涉及真实系统时,请务必在你拥有或已获授权的目标上测试。
4.4 签名还原之后的完整验证链路
签名算法确认之后,完整验证链路应该闭环:
- 用当前时间戳、随机nonce、真实业务参数生成一个新的sign
- 把完整的请求发送到服务端,看是否正常返回业务数据
- 如果返回正常,说明整个签名链路的理解是正确的
这个闭环实验非常关键。它能证明你不是碰巧撞对了一次,而是真正吃透了整个机制。同时,它也是检验参数过滤规则理解是否完整的终极标准——某个参数在抓包里看到了、但实际不参与签名,你把它加进拼接串,签名就会错。反过来,某个隐藏参数参与签名但你没发现,签名照样对不上。
很多人在这个环节翻车,最常见的现象是:脚本里算出来的签名跟抓包完全一致,但一发请求就报签名错误。这种情况通常踩中了编码坑——URL编码、大小写、空值处理,任何一个不对都能让服务端算出来的结果跟你不一样。
5. 常见问题排查与避坑指南
5.1 抓包环境里的高频问题
做这类分析,大量时间会耗在环境问题上,真正分析算法的占比反而不高。我把几个高频问题整理成了表格,都是实际踩过的:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 小程序请求抓不到 | 安卓7.0+不信任用户证书 | 把证书装进系统目录,或使用已配置好的模拟器 |
| 抓到的全是CONNECT请求 | SSL代理没有生效 | 检查SSL Proxying是否开启,域名是否在代理列表内 |
| 请求返回400或403 | 签名时间戳过期 | 确认脚本里用的是当前时间,不是抓包时的旧时间戳 |
| 接口返回"签名错误" | 拼接字段与校验字段不一致 | 逐字段排查,确认哪个参数参与签名、哪个被过滤 |
| 请求正常但返回加密数据 | 业务层有二次加密 | 定位解密函数,一般在响应拦截器里 |
还有一个容易忽略的点:小程序的部分请求走的可能是WebSocket或其他通道,跟普通HTTPS混在一起。定位时要留意Content-Type和请求方法,GET、POST、PUT都可能存在,别只盯着POST看。
5.2 混淆代码的应对思路
现在不少小程序都会做一定程度的混淆。碰到重度混淆时,硬啃代码效率极低,我一般换几条思路:
- 动态调试:在小程序运行环境里hook关键函数,观察运行时行为
- 行为分析:不分析代码,直接分析网络层的输入输出关系
- 控制变量法:每次只改一个参数,观察签名结果的变化规律
控制变量法在应对复杂算法时特别好用。怀疑某个参数参与拼接,就单独改这个参数的值,看签名变化还是不变。变了,确认在签名范围内;没变,说明被过滤或压根没参与。用这种方式,即使完全不看代码,也能把签名规则摸个大概。
另外,搜索关键词别只盯sign。secret、key、token、appKey、publicKey这些词都值得一试。密钥往往以字符串常量的形式出现在代码某个角落,找到它往往只是时间问题。代码里搜不到,就去内存里找,去请求头里找,密钥出现的形态是多种多样的。
5.3 签名还原中最容易忽略的细节
几个容易被忽略的细节,我单独拿出来说,都是踩过坑的:
第一,参数拼接时的空值处理。不少算法会先过滤空字符串、null、undefined,或者剔除某些特殊字段。过滤规则和你在代码里看到的不一定完全一致,需要逐项验证。
第二,数组和对象参数的序列化方式。参数值是数组或对象时,拼接形式可能是a=1,2,3,也可能是a=["1","2","3"]。序列化方式不同,拼出来的字符串完全不一样,签名结果自然对不上。
第三,编码与大小写。中文参数需要URL编码,编码前后的字符串完全不同。签名的输出也有大小写之分,有的算法输出大写十六进制,有的是小写。这个细节不仔细看,往往要排查半天。
第四,请求头里的参与字段。有时候签名范围不限于请求体,User-Agent的某一段、X-Wx-Token之类的自定义请求头,也可能参与签名计算。所以分析的时候别只盯着请求体,请求头同样要过一遍。
6. 安全视角的思考与合规边界
6.1 从逆向到防护的思维转换
做逆向本身,目的不该只是拿数据。我花时间梳理这类课题,更多是为了站在攻击者视角理解防护方案的薄弱环节,再反过来强化自己的系统设计。说白了,不懂攻击的防守,多半是纸糊的。
如果你正在设计小程序接口,下面几条建议可以直接落地:
- 签名密钥不要写死在客户端代码里——只要客户端能拿到,就一定能被分析出来,只是时间成本的问题
- 尽量用HMAC而不是简单MD5拼接——HMAC的抗碰撞能力更强,密钥管理也更规范
- 时间戳容忍窗口建议控制在5分钟以内,并且配合nonce去重
- 核心业务接口考虑加SSL Pinning作为第二道防线
- 服务端做好限流和风控,同一账号高频请求要能触发告警
没有绝对安全的客户端,只有层层叠加、让攻击成本高过收益的防护体系。
6.2 逆向分析的合法边界
这个部分必须说清楚。逆向分析本身不是违法行为,但使用方式直接决定合规性。安全领域有个基本共识:对你自己拥有或已获书面授权的系统做研究是合规的;未经授权对他人商业系统做攻击性测试、绕过认证、批量爬取数据,则可能触碰法律红线。尤其是涉及个人隐私、商业机密的场景,风险远大于收益。
所以如果你是在做自己的小程序安全测试、学习接口安全原理、或者参与合规的众测项目,这套思路完全适用。但如果你只是想拿别人的数据搞灰色业务,那这条路走不通,也不应该走。
6.3 这套方法论还能迁移到哪些方向
签名机制的分析方法,不只适用于航班查询类小程序。电商小程序、社交应用、企业办公系统,底层架构思路大同小异。掌握了这套方法论,换个目标只是按同样的流程重新走一遍而已。
再往外延伸,还能接触到更有意思的课题:代码混淆对抗、反调试机制、风控系统设计、服务端签名校验的高性能实现。每个方向深入下去,都足够独立成文。安全这行的特点就是这样,一个点扎下去,能带出一整片技术面。
我在实际研究这类问题时最大的体会是:破解签名机制这件事,真正的难点从来不在加密算法本身——MD5、SHA256、HMAC都是公开算法,文档一抓一大把。难点在于你要极有耐心地处理参数顺序、过滤规则、编码方式这些看似不起眼的细节。差一个字段对不上,整个签名就是错的,没有任何中间状态。这种"全对或全错"的特性,逼着人变得严谨,也算是一种额外的收获。
如果你此刻正卡在某个签名验证的超时或错误上,我的建议是:先把心静下来,把抓包数据一格一格对照,从排序、过滤、拼接、加密四个环节逐个排查。方法永远是那套方法,剩下的是细心问题。希望这篇内容能帮你少走一些弯路,也欢迎你在实操中遇到有意思的细节时,回来一起交流。