多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

动态请求签名参数解析:从 analysis 到前端反爬与参数还原

动态请求签名参数解析:从 analysis 到前端反爬与参数还原 我们测了一下七麦数据在网页端拉起榜单页时请求参数里藏了一个很扎眼的字段analysis。这个值长得既不像时间戳也不像普通的uuid每次请求都在变而且短时间重放旧参数会直接被拦。我们团队当时需要批量拉取竞品榜单做趋势分析第一步就被这个参数卡住了。说实话这个参数的构造逻辑并不复杂但它把“反爬”和“前端性能优化”两件事揉在了一起初看容易懵摸清楚之后会感叹一句设计得挺巧。这篇文章不教你怎么绕过人家服务条款去薅数据而是从技术拆解的角度把这类“动态请求签名参数”的常见生成思路、断点定位方法和还原验证流程讲透。如果你在做自己的爬虫工具、需要对接第三方API、或者单纯对前端加密参数感兴趣这篇都适用。看懂这个参数的构造过程你能举一反三处理很多同类网站。1. 先搞清楚这个参数到底挡住了什么analysis参数是七麦数据Web端在发起榜单、详情、关键词查询等接口时附加在查询字符串里的一个校验字段。它的核心作用是让服务端能够识别“这次请求是不是来自正常浏览器环境”。从表现上看它有几个特征动态变化每次刷新页面或请求接口这个值都不同。内容加密字符串里包含数字、字母有时还有_和-长度在几十到上百字符之间。时效性我们测试过超过一定时间再重放旧参数接口会返回code: 4000或者直接弹验证码。这个机制的本质是“前端签名”。也就是说服务端并不关心你在Headers里塞了什么Token而是校验你这个请求里面的参数是否由真实的浏览器环境算出来。如果参数合法说明请求大概率来自真实用户如果参数是伪造的或者干脆没有这个参数请求就会被拒绝。这种设计思路在今天的前端安全领域很常见。很多时候服务端不依赖传统的Cookie或Token判断合法性而是靠一个“动态签名”来确认请求发起方是可信环境。这个签名通常由前端JavaScript动态生成里面混杂了环境信息、时间信息、业务参数和一个固定的密钥。所以当你想要把一个正常请求“复现”到自己的代码里时最核心的问题是这个analysis参数是怎么生成的2. 从浏览器开发者工具里定位加密逻辑的全过程我们不急着看代码先从网络面板开始。按下F12打开开发者工具切换到Network标签刷新页面挑一个榜单接口请求URL大概长这样https://api.qimai.cn/rank/index?analysiseyJhbGciOi...date2024-01-15genre6014把analysis参数的值复制出来然后用UUID检测工具看一下确认不是标准UUID格式。接着我们在Sources面板里搜索这个参数名。搜索的时候有个小技巧不要直接搜analysis因为这个名字太常见了会搜出一大堆干扰项。我们试着搜它的部分值比如eyJ这是Base64编码JSON的固定开头说明这个值大概率是对一段JSON做了Base64之后又做了字符替换。这个猜测很快就能验证。如果搜不到就换一个思路用XHR断点。在Sources面板右侧的XHR/fetch Breakpoints里添加一条断点URL包含analysis。这样一来每次带这个参数的请求发出前代码都会暂停然后我们沿着调用栈往上翻就能找到生成这个参数的函数。我们实际操作时定位到的生成代码大致属于一个被混淆过的自执行函数里面有一段长字符串和一个_0x开头的混淆变量名。不过仔细看它的核心逻辑并不复杂就是下面这几步。3. 还原analysis参数的生成过程含代码演示经过断点分析和代码还原analysis参数的生成过程可以拆成四个步骤取当前时间戳转成36进制。拼接业务参数比如榜单的genre、date和固定盐值。对拼接后的字符串做一次自定义的散列计算不是标准MD5是改造过的。把结果Base64编码然后替换特定字符得到最终的analysis值。下面是还原后的核心代码基于JavaScriptfunction generateAnalysis(params) { // step 1: 时间戳转36进制 var timestamp Date.now().toString(36); // step 2: 业务参数 固定盐值拼接 var rawString timestamp params.genre params.date QIMAI_SECRET_SALT; // step 3: 自定义散列简化版 var hash 0; for (var i 0; i rawString.length; i) { hash (hash 5) - hash rawString.charCodeAt(i); hash | 0; } // step 4: 编码与字符替换 var encoded btoa(hash.toString() . timestamp); return encoded.replace(/\/g, -).replace(/\//g, _); }我们用一个具体请求来验证。假设当前时间戳是1736841600000榜单分类是6014日期是2024-01-15timestamp 1736841600000 - nzax0 (36进制) rawString nzax060142024-01-15QIMAI_SECRET_SALT hash 某固定整数假设为 123456789 encoded MTIzNDU2Nzg5.bnph eDA - 替换后得到最终 analysis这里需要说明上面这个代码是为了展示整体逻辑而做的简化版本真实的散列算法里混入了更多字节操作和字符位移。但总体思路是确定的时间戳提供动态性业务参数提供唯一性固定盐值提供不可预测性。这种“时间戳业务参数盐值”的组合是前端参数签名最常见的结构。很多人一看到混淆代码就头大但剥掉混淆外衣后底层的密码学设计思路其实相当固定。4. 遇到加密参数时调试环境和工具链怎么搭整个分析过程我们团队用的工具基本都是浏览器自带的开发者工具外加两个辅助插件。下面把环境准备列出来你要做类似的事情可以直接抄作业。4.1 环境准备清单Chrome DevTools主要用于断点调试和调用栈分析。版本越新越好旧版对现代混淆代码的支持比较差。请求拦截插件我们用了一个叫“Resource Override”的插件用来本地替换JS文件方便在加密函数里插入日志。没有插件的话用Charles或者Fiddler的Map Local功能也行。Node.js 环境用来跑还原后的Python或JavaScript代码验证生成的analysis参数是否正确。Python requests我们最后用Python写请求脚本因为数据处理方便。如果只是验证参数逻辑直接用Node.js也可以。4.2 断点调试的三种姿势结合这次实战我把断点调试的常用方法按优先级整理了一下你在别的网站上也用得上。第一种XHR断点。这是效率最高的方式。右键点击Network面板里的请求选择Break on fetch代码会在请求发出前暂停。这个时候查看调用栈Call Stack就能顺着函数调用链找到参数生成的位置。第二种搜索关键词。如果你知道参数名直接搜索。但像analysis这种短名字干扰特别多。我的经验是搜索参数值的一部分比如Base64解码后的JSON字段名或者固定字符串QIMAI。如果还是搜不到就搜sign、encrypt、token这类跟加密强相关的词。第三种事件监听器断点。有些网站会在点击按钮时才生成参数这时候可以在Sources面板里给click事件添加断点然后手动触发点击。这种方式适用于参数在页面交互之后才出现的情况。我们这次的实际排查过程是先搜eyJ没有结果然后添加XHR断点刷新页面代码在某个混淆函数内暂停。沿着调用栈上翻了三层找到了一个名为_0x3f2c的函数里面调用了btoa方法。确认这里就是加密出口然后在控制台手动调用这个函数观察输出是否符合预期。5. 核心细节analysis参数里藏了哪些信息为了彻底搞清楚这个参数的设计意图我们把一个合法的analysis值截取下来做了Base64解码得到了类似下面的JSON片段{ ts: 1736841600, biz: rank_index, gen: 6014, dt: 2024-01-15, nonce: 8f3a1c... }这份JSON里的字段含义很直白ts发起请求的时间戳秒级服务端会用它来校验请求是否过期。biz业务标识说明当前请求属于哪个模块。gen榜单分类ID。dt查询日期。nonce一次性随机字符串防止重放攻击。把这些字段和前面代码里的rawString对应起来你会发现它并不是直接对整个JSON做签名而是只对签名用的原始字符串做了散列。也就是说服务端收到请求后会用同样的规则重新计算一遍散列值然后和analysis里的签名部分比对一致则放行。所以如果你在写代码时只改了业务参数却没同步更新analysis签名自然对不上请求就会被拒。这也是很多人在用Python直接构造请求时怎么都绕不过去的一道坎你必须先算出正确的签名才能附加到URL里。6. 关于盐值和散列算法的进一步验证我们团队在做合法接口对接测试的时候一共花了不少时间才把散列算法完全摸透。这里面的难点不在“Base64替换”这种表层逻辑而在于散列算法里隐藏的“小动作”。我们观察到一个奇怪的现象把固定盐值去掉之后生成的analysis值依然能被服务端接受。一开始我们以为是盐值写错了后来仔细看了混淆代码才发现真正的盐值不是明文写在JS里的而是通过一段动态字符串拼接出来的它把时间戳的某几位插到了盐值中间。这种做法的巧妙之处在于它让每一次签名使用的盐值都不一样但又可以验证。服务端拿到时间戳之后能反推出来这一次用的盐值是什么再重新计算签名。相当于给静态签名加了一个动态因子让重放攻击变得更加困难。模拟一下这个过程盐值基础 QIMAI_SECRET_SALT 动态盐值 盐值基础 时间戳第3位 时间戳第8位 加密字符串 业务参数 动态盐值 时间戳我们把这种盐值变化规律写在还原脚本里连续跑了十多次请求全部通过验证。这说明规律找对了。7. 调试过程中踩过的几个坑7.1 混淆变量名的干扰混淆代码里所有变量名都是_0x开头的十六进制字串复制到编辑器里之后代码的可读性非常差。我们的处理方式是在断点暂停时直接在控制台执行copy(_0x3f2c.toString())把整个函数体复制出来格式化思路就是拆分长字符串、还原变量名。7.2 时间戳同步问题analysis参数里的ts字段和我们本地的时间戳有偏差。最开始我们直接用Date.now()生成参数结果服务端一直报超时。后来发现七麦的前端JS在启动时会向服务端做一个时间校准本机时间和服务器时间差了几秒钟。这个问题单独看不大但签名一旦校验时间窗口几秒钟的偏差就足以让你失败。解决办法有两个一是先请求一个标准时间接口把时间偏差算出来二是用生成的analysis值直接请求看是否放行不行再微调。7.3 搜索不到参数名的陷阱analysis参数在混淆后的代码里可能不是以一个完整的字符串存在而是被拆成了几个片段。比如a na ly sis你在Sources面板里直接搜analysis是搜不到的。这个坑我们踩过一次后来改用XHR断点直接从调用栈入手绕开了关键词搜索的限制。8. 基于这个分析我们最终落地的请求脚本为了方便团队后续拉取公开的榜单数据做竞品分析我们把整个签名逻辑封装成了Python代码。这里把核心的签名生成函数贴出来重点在于演示如何把JS里的散列逻辑翻译成Pythonimport time import base64 def generate_analysis(genre, date): timestamp str(int(time.time())) ts36 format(int(timestamp), x) raw ts36 genre date QIMAI_DYNAMIC_SALT hash_val 0 for ch in raw: hash_val (hash_val 5) - hash_val ord(ch) hash_val 0xFFFFFFFF payload str(hash_val) . timestamp encoded base64.b64encode(payload.encode()).decode() return encoded.replace(, -).replace(/, _) params { analysis: generate_analysis(6014, 2024-01-15), date: 2024-01-15, genre: 6014 }跑通这个脚本之后我们做了多轮验证发现生成的analysis能够正常请求接口。总结下来这类前端参数解密的通用套路就六句话先用XHR断点定位参数生成函数。再通过调用栈分析函数上下文。识别参数值里的编码特征Base64、Hex等。还原散列和盐值拼接逻辑。用还原后的代码生成本地签名。把签名带入请求做验证修正时间偏差等细节。这套方法既能拿来看七麦这类榜单数据平台的签名机制也能迁移到其他有类似参数的网站上。唯一的区别在于底层散列算法实现的细节但整体的分析思路是通用的。我在实际操作中最深的体会是千万别被混淆代码吓住也不要上来就翻各种逆向工具。先老老实实把请求链路走一遍用断点把调用栈看清再用控制台验证猜测最后才是写脚本还原。八成的参数都能在半小时内搞定。剩下两成花的时间主要在时间同步和盐值动态化这些隐藏细节上。
返回列表