我们测了一下七麦数据在网页端拉起榜单页时,请求参数里藏了一个很扎眼的字段:analysis。这个值长得既不像时间戳,也不像普通的uuid,每次请求都在变,而且短时间重放旧参数会直接被拦。我们团队当时需要批量拉取竞品榜单做趋势分析,第一步就被这个参数卡住了。说实话,这个参数的构造逻辑并不复杂,但它把“反爬”和“前端性能优化”两件事揉在了一起,初看容易懵,摸清楚之后会感叹一句:设计得挺巧。
这篇文章不教你怎么绕过人家服务条款去薅数据,而是从技术拆解的角度,把这类“动态请求签名参数”的常见生成思路、断点定位方法和还原验证流程讲透。如果你在做自己的爬虫工具、需要对接第三方API、或者单纯对前端加密参数感兴趣,这篇都适用。看懂这个参数的构造过程,你能举一反三处理很多同类网站。
1. 先搞清楚这个参数到底挡住了什么
analysis参数是七麦数据Web端在发起榜单、详情、关键词查询等接口时,附加在查询字符串里的一个校验字段。它的核心作用是让服务端能够识别“这次请求是不是来自正常浏览器环境”。
从表现上看,它有几个特征:
- 动态变化:每次刷新页面或请求接口,这个值都不同。
- 内容加密:字符串里包含数字、字母,有时还有
_和-,长度在几十到上百字符之间。 - 时效性:我们测试过,超过一定时间再重放旧参数,接口会返回
code: 4000或者直接弹验证码。
这个机制的本质是“前端签名”。也就是说,服务端并不关心你在Headers里塞了什么Token,而是校验你这个请求里面的参数是否由真实的浏览器环境算出来。如果参数合法,说明请求大概率来自真实用户;如果参数是伪造的,或者干脆没有这个参数,请求就会被拒绝。
这种设计思路在今天的前端安全领域很常见。很多时候,服务端不依赖传统的Cookie或Token判断合法性,而是靠一个“动态签名”来确认请求发起方是可信环境。这个签名通常由前端JavaScript动态生成,里面混杂了环境信息、时间信息、业务参数和一个固定的密钥。
所以,当你想要把一个正常请求“复现”到自己的代码里时,最核心的问题是:这个analysis参数是怎么生成的?
2. 从浏览器开发者工具里定位加密逻辑的全过程
我们不急着看代码,先从网络面板开始。按下F12打开开发者工具,切换到Network标签,刷新页面,挑一个榜单接口,请求URL大概长这样:
https://api.qimai.cn/rank/index?analysis=eyJhbGciOi...&date=2024-01-15&genre=6014把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值。
下面是还原后的核心代码(基于JavaScript):
function 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-15:
timestamp = 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里的散列逻辑翻译成Python:
import 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等)。
- 还原散列和盐值拼接逻辑。
- 用还原后的代码生成本地签名。
- 把签名带入请求做验证,修正时间偏差等细节。
这套方法既能拿来看七麦这类榜单数据平台的签名机制,也能迁移到其他有类似参数的网站上。唯一的区别在于底层散列算法实现的细节,但整体的分析思路是通用的。
我在实际操作中最深的体会是:千万别被混淆代码吓住,也不要上来就翻各种逆向工具。先老老实实把请求链路走一遍,用断点把调用栈看清,再用控制台验证猜测,最后才是写脚本还原。八成的参数都能在半小时内搞定。剩下两成,花的时间主要在时间同步和盐值动态化这些隐藏细节上。