做网页端数据采集和自动化的人,手里多少都会攒着几个“说不清名字的参数”。wsgsig就是我在某个出行平台的网页端里遇到的那一个。它挂在每个关键请求的URL尾巴上,名字一看就是缩写:sig是签名(signature),前面那一小段wsg大概率是内部模块或场景代号。换句话说,这地方做了参数签名校验,想靠“复制Cookie”或者“简单改改参数”去调接口,基本行不通。
这篇东西我不打算写成一篇“教你怎么绕过某某平台风控”的教程,那既不合规也没意思。我更想把它当成一次完整的参数逆向与工程化落地复盘来聊:从第一次在抓包里看到wsgsig,到定位它的生成逻辑,再到把黑盒还原成可复算的函数,最后聊清楚落地到自动化脚本时要处理的那些坑。适合正在研究各类网页端签名机制、或者想系统梳理“签名参数逆向”思路的朋友。
1. 初见wsgsig:藏在URL尾部的签名参数
1.1 第一次抓包时看到的画面
事情的开头很普通。我想基于某出行平台的网页端,做一些合规范围内的数据观察和分析。打开浏览器开发者工具,切到Network面板,刷新页面,随便触发一个搜索或者列表加载请求,然后一眼扫过去——URL末尾多了一截东西:
https://xxx.example.com/search?keyword=xxx&page=1&wsgsig=0f6b2a...这串参数长度不短,像是一段十六进制字符串,偶尔还带一些大写字母。第一反应是“又来了,签名”。为了确认它到底是不是必选参数,我先把wsgsig手动删掉,重新发送同一个请求,返回结果直接变成一个错误码,提示大概意思是“参数校验失败”。
到这里基本可以下一个结论:这个接口并不关心你有没有登录态,它关心的是请求参数有没有被篡改。也就是说,wsgsig不是权限凭证,而是一个完整性校验值——相当于给请求参数盖了一个章,服务端收到请求后自己再算一遍这个章,对不上就直接拒绝。
这种设计在很多网页端都能见到。不同平台的命名方式五花八门,有的叫sign,有的叫token,有的叫_signature,而这里的wsgsig是“模块前缀+signature缩写”的组合。理解了命名习惯,就不容易被参数吓到,本质上它就是一段可计算的字符串。
1.2 参数职责和基本特征推测
在正式动手逆向之前,我习惯先通过外部观察把候选参数的特征列出来,这样后面定位代码时才有方向。wsgsig的几个明显特征是这样的:
| 观察项 | 现象 | 推测 |
|---|---|---|
| 出现位置 | URL query末尾 | 生成时机在请求发出前 |
| 字符形态 | 十六进制+大写/小写混合 | 可能是MD5/SHA家族摘要 |
| 是否变化 | 请求参数或时间变化时随之变化 | 参与计算的输入是动态的 |
| 是否必填 | 删掉后请求被拒绝 | 服务端做了强校验 |
| 与Cookie关系不明显 | 无登录态时也存在 | 与Session无关,属于请求级签名 |
这里有个容易被忽略的细节:wsgsig并不是只在“需要登录”的接口里出现。有些平台的列表页、搜索页,哪怕你完全没登录,请求里也会带类似参数。这说明签名校验往往前置在网关层,用来拦截批量脚本和异常流量,而不是等到业务逻辑再判断。
另一个细节是,wsgsig在请求里的位置有时候会被URL编码处理。比如原始生成的字符串里如果带有+、=、/这些字符,放到URL里可能变成%2B、%3D、%2F。抓包的时候如果看到这类编码痕迹,恢复原始字符串时要注意解码,否则后面比对签名时会莫名其妙对不上。
2. 从调用栈倒推定位:把生成函数从混淆代码里挖出来
2.1 断点位置选在哪儿最省事
要找到wsgsig的生成逻辑,最直接的手段不是读完整份压缩后的JS,而是“在请求发出前截住它”。我习惯用Chrome开发者工具的Sources面板,给fetch或者XHR打一个全局断点。
操作路径很固定:打开Sources面板,右侧找到Event Listener Breakpoints,展开Fetch和XHR,把request勾上。这时候再去页面里触发一次搜索,浏览器会在真正发请求前停下来,调用栈里会完整展示“到底是哪一行代码把这个请求送出去的”。
关键技巧是:不要一上来就在调用栈最底层去找加密函数,而是先看当前作用域里有没有已经生成好的wsgsig参数。很多时候,断点停下来时,请求参数对象就躺在Scope面板里,里面已经包含完整的签名值。这样你至少先拿到了“参数输入和最终签名结果”的一对样本,后面判断算法会轻松很多。
如果断点停在的位置只能看到URL字符串,看不到参数对象,那就沿着调用栈往上走一层。通常发请求的函数会有类似request({ url, params })的结构,params里一定藏着wsgsig。在这一层再做一次断点,右键点击调用栈里的上层函数,选择Restart frame,可以重新执行一次这个过程,方便反复观察。
2.2 三个最磨人的干扰项
定位过程不会一路顺风。以我这次的经验,下面三个干扰项最耽误时间,提前知道能省不少功夫。
第一是压缩混淆。网页端的JS几乎都是压缩过的,变量名全是t、n、e这种单字母。函数一层嵌套一层,找起来很痛苦。应对办法是:先不看变量名,而是看字符串常量。因为签名算法里必然要拼接某些固定文本,比如盐值、字段名、特定标识。在代码里搜索wsgsig这个字符串本身,或者搜索接口路径的一部分,往往能直接跳到生成签名的附近。
第二是异步时序。有些平台把签名生成放在Web Worker里,或者在一个延迟执行的Promise回调里。单纯靠断点去“等”可能等不到。这时候可以在代码里搜索特征字符串,找到后用关键字断点(XHR/fetch Breakpoints)精确命中。也可以直接把生成签名的那段逻辑附近的代码打上日志断点,输出中间变量的值。
第三是加密SDK的包装。某些站点会引入自研的加解密SDK,对外只暴露一个encrypt()函数,内部做了一堆跳转。你以为找到了入口,结果进去发现还有十层。这种场景不用死磕SDK内部,先通过输入输出推测算法类型,再做交叉验证就好。
2.3 用“最小复现脚本”锁定生成入口
通常我不满足于“我只能在一个特定页面上让它跑起来”,我更希望有一个不依赖界面的最小复现方式。做法是:把生成的签名函数单独抠出来还不够,因为它的上下文依赖太多;更可靠的办法是,把请求参数传到页面已有的函数入口里,在浏览器控制台执行一遍,看能不能得到一致的wsgsig。
具体来说,找到生成wsgsig的入口函数后,用控制台手动构造一组参数,调用一次这个函数,对比它与真实请求里的参数是否一致。如果一致,说明你已经找到了正确的计算入口。之后要做的,就是把这块逻辑移植到你自己的环境里。
这里有个非常容易踩的坑:很多签名函数内部依赖全局对象,比如window、document、location。你在控制台里调用没问题,因为控制台本身就在页面环境里,但一旦把这些代码搬到Node环境,分分钟报错。所以第一版本的移植,尽量不要直接复制整个函数,而是“用页面环境做计算,用外部脚本做调度”,后面我会讲到这一点。
3. 还原可复算的逻辑:用对照验证把黑盒变成白盒
3.1 从指纹特征判断算法家族
拿到若干组“参数→签名”样本后,就可以开始判断算法类型了。判断依据不复杂,主要看长度、字符集和变化规律。
常见的摘要算法里,MD5输出32位十六进制,SHA1输出40位,SHA256输出64位。如果wsgsig的固定长度是32位,优先怀疑MD5或其变体;如果是40位或64位,往SHA家族靠。如果长度不固定,那可能是Base64编码后的加密结果,或者带自定义截断。
我当时看到的wsgsig是32位,偶尔全小写,偶尔混合大小写。这种风格很像是先算MD5,再做了一些字符串变换。另外我注意到,即使两次请求的参数完全相同,只要时间戳变化,签名也会变。这说明生成逻辑里至少包含一个动态因子,最常见的就是时间戳。
到这里只能说“很像MD5系”,不能下结论。真正确定算法,还是通过复现验证来反向确认。
3.2 我的“复算验证三步法”
这一步是整个逆向过程里最有价值的环节。我把它总结成三步,每步都有明确的通过标准。
第一步是固定输入复算。取一次真实请求里参与计算的参数(字段名与字段值)和时间戳,用候选算法(比如MD5)计算出一个结果,和请求里实际的wsgsig比对。一致,则进入下一步;不一致,检查拼接顺序、大小写、是否包含额外盐值。
第二步是改动敏感性验证。把参与计算的某个参数值改一个字符,观察离线的签名计算结果是否完全变化。好的摘要算法应该对输入极为敏感,任意改动都会导致结果面目全非。如果改了明文字符后,签名只有局部变化,说明内部可能做了分段处理,或者根本不是普通摘要。
第三步是批量抽样比对。找10到20组真实请求,提取每组的时间戳、参数、最终签名,用离线的还原逻辑批量计算,比对一致率达到100%才算合格。如果只是样本里碰巧对上一两次,没有说服力。
这三步走完,基本上可以确认“黑盒”已经变成了“白盒”——至少在这一版前端代码里,你知道了签名是怎么来的。
3.3 撞上的几个“伪结论”
还原过程中很容易推断出错误结论。我踩过最典型的两个坑,一个是“误以为拼接顺序是字典序”,另一个是“误以为时间戳是秒级”。
网站上很多签名逻辑喜欢先把参数名按字典序排序再拼接,这是比较常见的习惯。但这个平台的生成逻辑里,有一组参数顺序是按请求体的原始顺序来的,并非字典序。如果我一开始就默认排序,后面所有的验证都对不上。排查方法很简单:手动构造两个只有顺序不同的参数组合,分别看结果是否一致,就能判断排序逻辑。
时间戳的坑则更隐蔽。请求里能看到类似timestamp=1678...这种秒级时间戳,但签名计算时用的可能是毫秒级时间戳。你拿秒级时间戳参与计算,网上怎么都对不上,后来把时间戳换成毫秒级,一次性成功。从那以后我养成了一个习惯:凡是签名对不上,先怀疑时间精度。
这类“伪结论”非常消耗时间,但也能帮你积累直觉。到后面你看到一个签名,基本能预判出它内部大概做了哪几步变换。
4. 把签名能力工程化:两条落地路线与关键取舍
4.1 受控浏览器方案:让站点代码自己算
算法还原成功之后,面临的问题变成“怎么稳定地拿到有效签名”。最稳妥的方案是直接在受控浏览器环境里跑页面自带的签名逻辑,完全不去碰内部实现。
我当时用的是Playwright做了一套受控浏览器脚本。核心逻辑很简单:打开目标页面,等待页面里加载完包含签名生成逻辑的脚本,然后在page.evaluate()里调用页面上已经存在的签名生成函数,把参数传进去,拿到返回值再拼到请求里。
这条路线的最大优点是“以不变应万变”。只要页面自己还能正常运行,你就不需要维护算法逻辑。哪怕平台调整了拼接顺序、换了盐值,只要前端代码同步更新了,你每次重新加载页面拿到的都是最新版本逻辑。
但缺点也明显:受控浏览器的资源开销大,单位时间里能发起的请求量有限;而且页面依赖的首次渲染和脚本加载都需要时间,不适合高并发场景。另外,有些页面会做WebDriver检测,受控浏览器容易被识别出来,这属于另一个层面的问题,会在后面提到。
4.2 脚本重写方案:去掉浏览器这个沉重包袱
如果目标平台页面体量很大,或者你需要在服务端批量跑任务,受控浏览器方案就不太够用了。这时候更适合把签名逻辑用脚本语言重写一遍,直接在服务端计算。
以Python重写为例,最终代码结构大概长这样:
# 示意代码,仅为说明逻辑结构,不代表任何目标站点的真实实现 import hashlib import time def calc_wsgsig(params, salt): # 1. 按约定顺序拼接待签名文本 raw_string = build_raw_string(params, salt) # 2. 取毫秒级时间戳参与计算 timestamp = int(time.time() * 1000) # 3. 做摘要并截取 md5_once = hashlib.md5((raw_string + str(timestamp)).encode("utf-8")).hexdigest() # 4. 二次摘要后再做字符串变换 final = hashlib.md5(md5_once.upper().encode("utf-8")).hexdigest() return final[8:24].upper()上面这段代码只是示意。实际工程中,你需要把真实的拼接规则、盐值、变换步骤都维护到独立的配置模块里。脚本重写方案的优势是轻量、可控、速度快,一百个请求可以在很短时间内完成签名计算和发送。
但它的成本也不低:一旦平台端改了算法,你就得重新逆向、重新开发、重新测试。另外,平台可能同时存在多个前端版本或AB测试变体,不同版本签名逻辑可能不同,你需要设计一份“版本选择”的逻辑来兜底。
4.3 工程化里绕不开的四件事
无论选哪条路线,只要把签名能力做到工程级应用层,下面四件事一定会遇到。
第一是时效管理。签名通常带有隐含的有效期,有的短到一两分钟,有的长达半小时。你需要根据实际测试确定这个窗口。可以在本地记录签名生成时的时间戳,超过有效期就重新计算。不要把所有请求都等失效后再重试,那样浪费太多时间。
第二是缓存复用。当并发比较高时,不要每发一个请求就重新生成一次签名。同一个时间窗口内,相同参数集合的签名可以缓存复用。尤其当签名算法里有基于当前时间戳的动态因子时,缓存能大幅减少重复计算。但注意:既然时间戳参与了计算,缓存窗口就必须小于平台侧的有效期,否则你缓存的签名已经失效。
第三是并发控制。受控浏览器方案里尤其要注意,别让多个线程同时调用同一个页面实例去生成签名,这会导致上下文冲突。一般的做法是加一个互斥锁,或者维护一个签名生成的连接池,每个浏览器实例独立处理请求。
第四是失效兜底。网络环境里什么事情都可能发生。你的脚本必须能识别“签名失效”的返回特征。通常平台会返回特定的业务错误码,或者HTTP状态码加一段固定的错误信息。把这个识别逻辑做进去,遇到签名失效时自动刷新签名并重放一次请求,整体稳定性会上一个台阶。
这部分内容看似零碎,但恰恰是工具落地时最花时间的环节。算法本身也许一天就能还原出来,工程层面的稳定性却需要反复打磨。
5. 签名之外的隐形关卡:风控识别与合规边界
5.1 参数签名只是第一道门
当你把wsgsig处理明白之后,大概率会有一个错觉:只要签名正确,就能自由地向接口发起请求。但真的这么做,很快会撞上另一道墙——风控。
签名校验的本质是“参数完整性验证”,它告诉服务端“这批参数没有被篡改”。但服务端还会持续观察你这批请求是不是由真实用户发出的。判断依据包括单位时间内的请求频率、请求来源IP、TLS指纹、HTTP头的顺序、浏览器环境特征,甚至包括你的鼠标轨迹和滚动行为。
我实测下来,签名正确只能保证“请求本身能通过参数校验”,并不代表请求会被正常受理。一旦频率过高,平台不会直接拒绝请求,而是可能让你进入验证码流程,或者返回一些看似正常但内容已经过脱敏处理的数据。这种软性的指标比签名本身更难处理,而且持续对抗它既不体面也不稳定。
所以如果你做自动化不是为了学习,而是为了长期稳定地获取某类数据,更理智的做法是优先研究平台是否有官方的开放接口,或者通过正规渠道申请数据权限。签名逆向更适合用在验证一个思路、做短周期的技术调研,以及自己账号下的数据整理与备份。
5.2 合规边界和我自己的使用原则
写到这里,我想把话说得更直接一些。参数逆向本身是一种中性的技术能力,就像一把螺丝刀,可以用来拆电脑,也可以用来拆不该拆的东西。关键要看使用的场景和边界。
我个人的使用原则很简单:只在自己拥有合法权限的范围内做验证和测试,不批量抓取任何涉及他人隐私或商业敏感的数据,不做绕过付费、恶意压测、干扰平台正常运行的事情。如果某个需求超出了这条边界,我会停下来重新评估方案。
具体到操作上,我也做了一些限制。比如本地验证用的都是自己可控的测试账号;生成的请求量控制在一个极低的频率,不影响平台的正常服务;脚本也不对外开放、不做商用。技术研究是为了搞清楚原理,而不是为了无限度地获取数据。
从逆向wsgsig这个参数开始,到最后理解整套校验与风控的架构,这个过程的价值不在于你“拿下”了一个平台的签名,而在于你对网页端请求的安全设计有了整体的感知。以后再遇到其他平台的签名参数,你会知道该从哪里入手、如何验证、如何落地,也知道哪些线不能碰。这种系统性的认知,比某个具体参数的计算方法有用得多。