前阵子在分析某出行平台网页端的时候,我盯着一个叫 wsgsig 的请求参数看了很久。如果你也做过这类平台的接口调试,大概率会在翻请求头或者 params 的时候撞见它——一串长得有点像 Base64、又混着十六进制字符的字符串。第一次见的人很容易把它当成普通的 token 或者 session id,但实际上它是整个请求链路里最关键的一层签名校验。我当时最早排查问题的时候,就是因为忽略了它的生成规则,导致模拟请求一直报参数错误,折腾了两天才找到症结。
这篇文章我不打算堆理论,就围绕 wsgsig 这个网页参数,把它的定位方式、生成逻辑的通用分析思路、以及我在实际调试中踩过的坑,完整梳理一遍。内容适合正在做网页端接口分析、前端加密参数逆向、或者对风控签名机制感兴趣的开发者阅读。我会尽量把分析过程拆成可操作的步骤,让你在自己电脑上也能顺着这套思路把问题跑通。
1. 先搞清楚 wsgsig 是什么,以及它为什么重要
1.1 从一次失败的模拟请求说起
当时我在本地用脚本模拟某出行平台的网页端行为,目标是拉取一个基础的列表数据接口。肉眼观察浏览器的请求,发现 query string 里除了常规的 page、city、keyword 之外,还有一个长字符串,参数名就叫 wsgsig。我一开始想当然地认为它只是某种固定的令牌,于是把它复制粘贴到脚本里,结果请求直接返回 403。更让人恼火的是,同样的参数复制到浏览器的另一个 Tab 里重放,居然也会失效。
这个现象本身就说明了一个关键问题:wsgsig 不是一个静态值,它和请求的其它参数、时间、甚至运行环境有强绑定关系。从名字猜测,sig 是 signature(签名)的缩写,wgs 大概率是某个业务模块的代号前缀。这种命名方式在很多互联网大厂的前端工程里都很常见——每个业务线用自己的缩写打头,后面跟功能后缀,比如 xxxsig、xxxtoken、xxxid。
1.2 这类签名参数解决的问题
理解了命名,就要理解它存在的目的。在网页端,后端接口并不能天然相信来自浏览器的每一次请求。如果完全不加防护,任何人都可以构造请求轻松拉走全量数据,甚至通过批量刷接口来影响业务。于是前端工程师会在请求发出前,利用一段 JavaScript 代码把请求参数、时间戳、设备标识等信息进行拼接和加密,生成一个签名串。这个签名串随着请求一起发送到后端,后端用同样的算法和密钥重新计算一遍,比对结果是否一致。如果不一致,直接判定为非法请求并拒绝服务。
wsgsig 扮演的就是这个“签名”角色。它的存在,意味着任何想正常调用该平台接口的程序,都不能绕过前端 JS 直接裸调接口,必须先把生成签名的那段逻辑完整复现出来。这既是风控的一部分,也是技术分析的突破口。
1.3 分析 wsgsig 前的准备工作
不管你是出于学习目的还是工作需要,在开始分析之前都要把环境准备好。我建议至少准备以下几样东西:
- 一台装了最新版 Chrome 或 Edge 的电脑,最好控制变量,不要开太多无关插件,否则会在分析调用栈时增加噪音。
- Chrome DevTools 要熟练使用,特别是 Sources 面板的断点调试、Call Stack 查看、Scope 变量监视。
- 一个轻量级的本地抓包工具,比如 Fiddler 或 Charles,也可以直接用 Chrome 自带的 Network 面板,但有些场景下需要修改请求做重放验证,抓包工具会更方便。
- 如果涉及 JS 代码阅读和格式化,可以用 webpack 工程里常见的 sourcemap 还原,或者直接用 Beautify 插件把压缩代码格式化。
准备工作不需要太复杂,核心就一句话:你需要在浏览器里观察到请求的完整生命周期,并且能控制它执行到一半停下来,看看到底是谁、用什么东西、生成了这个 wsgsig。下面我从头到尾演示一遍这个过程中的关键思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 网页参数定位的完整思路:从 Network 到调用栈
2.1 第一步,锁定 wsgsig 的发起上下文
打开目标网站,按 F12 进入 DevTools,切到 Network 面板。随便触发一个会发请求的操作,然后在请求列表里找到带有 wsgsig 参数的请求。右键这个请求,选择“Copy -> Copy as fetch”或者直接复制请求链接,把 URL 里的参数拆解清楚,确认 wsgsig 所在的完整字符串长什么样。
这一步的核心目的,是确认参数是出现在 query string、request body、还是 header 里。wsgsig 在我分析的场景中出现在 query string 里,这意味着它在 URL 上就能看清楚整体结构。出现位置会影响后面的搜索策略:如果参数出现在 request body,你需要在 Initiator 里找 XHR/fetch 的调用位置;如果出现在 header,可能还要看有没有额外的拦截器在处理。
拿到请求后,先看它的 Initiator 列。Chrome 会显示这个请求是由哪一段 JS 触发的。点击 Initiator 里的调用位置,会直接跳转到 Sources 面板对应的 JS 文件行号处。这时候千万别急着往下看,先在行号位置打一个断点,再重新触发一次请求,让程序停在发起请求的前一刻。接下来,在右侧 Scope 面板里查看当前作用域的变量,找有没有直接与 wsgsig 相关的变量。运气好的话,能直接看到变量名;运气不好,它只是作为参数对象的一个属性传进来了。
2.2 第二步,用搜索功能快速定位加密函数
如果在调用栈里没有直接看到 wsgsig 的赋值过程,那就需要在 JS 源码里搜索关键字。点击 DevTools 右上角的三个点菜单,选择 Search,或者直接用快捷键 Ctrl+Shift+F(Mac 上是 Cmd+Option+F),搜索 wsgsig。这个操作会在当前页面加载的所有 JS 资源里做全文匹配。
搜索结果的命中位置通常会告诉你两件事:一是 wsgsig 在哪些地方被读取或写入,二是生成逻辑是在哪个文件、哪个函数里被调用的。比如你可能会看到类似这样的代码片断:
javascript复制params.wsgsig = sign(param1, param2, timestamp)
或者更隐蔽的写法,比如把它藏在某个对象合并操作里:
javascript复制Object.assign(requestData, {
wsgsig: signFn(sortedParams)
})
搜索命中的地方不一定是加密算法本体,更有可能只是调用入口。真正重要的是顺着这个入口,找到定义 signFn 或 sign 函数的位置。此时先不要急着点击跳转,因为压缩混淆后的 JS 里函数名可能是一个看起来完全没有意义的变量名,比如 _0xabc123。你需要把包含该函数的整段代码复制出来,格式化之后再阅读。
2.3 第三步,理解调用栈的顺序
假设你在 signFn 函数的第一行打了断点,并且重新触发了请求。程序停住后,你会看到 Call Stack 里从底层事件监听、到某个业务封装函数、再到具体的签名函数,一路排列下来。这个顺序非常有用,因为它能让你搞清楚整个请求的发送链路,尤其是签名函数是在“请求参数组装完成后”还是“参数组装过程中”被调用的。
这个顺序决定了你在复现签名逻辑时,应该如何组织自己的加密代码。比如,如果 wsgsig 是在请求参数对象最终被 JSON.stringify 之前生成的,说明它在签名时可能把整个参数对象都作为输入;如果是在之后追加到 URL 上的,那它的输入可能只是一小部分参数,甚至只与时间戳和某个固定 key 有关。根据我的经验,大多数平台的签名参数都倾向于把关键参数拼进签名内容,这样可以防止请求参数被篡改。
3. 深入加密函数内部:识别算法与数据来源
3.1 从函数命名和外部依赖猜算法
当你定位到生成 wsgsig 的函数后,先别急着逐行读代码。先观察这个函数模块的整体结构,尤其是它引用了哪些全局变量、调用了哪些外部方法。很多时候,压缩混淆代码里会残留一些特征字符串或函数名,能够直接提示加密算法。比如看到 sha256、enc、Base64、HmacSHA1 这类字样,就能大致确认算法类型。
如果没有这些提示,就需要从输出长度来倒推。wsgsig 这类参数有一个很有意思的特征,就是它通常有固定的长度。如果生成结果是 32 位小写十六进制字符串,那大概率是 MD5;如果长度是 40 位,很可能是 SHA1;如果是 64 位,多半是 SHA256;如果字符串里既有大小写字母又有数字,还带着 +、/ 或 -、_ 这些字符,那很可能经过了 Base64 编码,底层可能是某种散列值或加密结果的 Base64 表达。
我当时分析那个平台返回的 wsgsig,长度大约在 96 到 128 位之间,而且每次请求都不一样,字符分布很均匀。加上它在 URL 里还会做 URLEncode 处理,所以单从输出很难直接判断。这种时候不要猜,要用代码定位。
3.2 断点追踪输入参数
在签名函数入口处打上断点,然后查看函数接收到的入参。重点观察这几个点:
- 入参中是否包含当前时间戳,一般是
Date.now()或new Date().getTime()。 - 入参中是否包含请求参数对象,如果是,那就需要搞清楚参数拼接顺序。
- 是否包含一个固定字符串或者从某个配置接口获取的密钥。
- 是否包含 cookie 里的某个字段。
我当时遇到的情况是,函数接收了三个入参:一个普通对象、一个时间戳、还有一个看起来像是从全局配置里读取的长字符串。普通对象里有 page、city 等请求业务参数,时间戳就是发送请求那一刻的时间,而那个长字符串则是在页面加载时从另一个配置接口返回的。到这里,大概的逻辑图已经出来了:把请求参数和时间戳拼在一起,加上固定密钥,经过某种摘要算法,输出成 wsgsig。
有一种特殊情况需要特别注意:有些平台的签名函数并不直接返回字符串,而是返回一个对象,之后再由外部代码转成字符串。断点时不要只盯签名函数内部,还要注意它的返回值后续被谁消费了。如果忽略了这一步,你可能会漏掉关键的处理环节。
3.3 动态得到还是静态写死:观察算法中的隐藏逻辑
当你进入加密函数内部,可能会看到这样的写法:它并不直接对明文字符串做散列,而是通过一个或多个自定义函数做了多轮变换。最常见的模式有两种:一种是“参数排序 + JSON 序列化 + 加盐 + 摘要”,另一种是“字符串替换 + 编码转换 + 多轮哈希”。理解这些模式比自己从头读代码要快得多。
第一种模式,通常是把请求参数对象的所有 key 排序,用 sort() 方法排序后,再用 JSON.stringify() 序列化,后面拼上时间戳或密钥。这种模式的特点是每个请求之间的签名值差异很大,但只要请求参数不变,签名值就会稳定。第二种模式则常在 URL 参数不明文传输的场景下出现,前端先把参数做一层自定义编码,再把编码结果作为签名输入。
分析时,最好把整个加密逻辑的流程序列化地画在纸上:输入是什么、经过第几步、产生了什么中间值、最后输出是什么。我在实际项目中会把每一步的中间结果用 console.log 打印出来,或者直接修改代码在浏览器里输出,这样可以非常快速地确认自己的理解是否正确。
4. 实操复现:从补环境到代码落地
4.1 为什么不能直接“抠代码”运行
很多新手在定位到加密函数后,第一反应是把它复制到 Node.js 里直接运行。但很快会发现问题:代码里用到了 window、document、navigator 等浏览器环境变量,一旦脱离浏览器,这些变量就全部不存在。于是程序直接抛错。这就是常说的“补环境”问题。
解决这个问题,有两个方向:
- 轻量级补环境:在 Node 脚本里手动定义缺失的浏览器全局变量。比如只用到了
navigator.userAgent,那就简单模拟一个:
javascript复制global.navigator = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
};
global.window = global;
这种做法适用于加密逻辑简单、只依赖少量浏览器 API 的场景。
- 重量级补环境:如果加密函数内部调用了复杂的 DOM 操作、Canvas 指纹、WebGL 信息,手动补环境基本不现实,这时候要用 Puppeteer 或 Playwright 这类自动化框架,直接用真实浏览器执行 JS,从页面上下文中抓取签名结果。
对于 wsgsig 这类参数,我更推荐从简到繁尝试:先手动补最简单的环境,看能不能跑通;不行再考虑用无头浏览器。
4.2 单步执行与本地复现
我的实际操作流程是这样的。先把包含 wsgsig 生成逻辑的 JS 文件完整保存下来,用 Prettier 格式化,再把签名函数单独抽离到一个测试文件里。然后写一段简单的 Node 脚本,模拟浏览器环境,调用签名函数。
这里有几个降本增效的小技巧。不需要把整个加密函数块理解得特别透彻,只要保证它能运行即可。比如代码中有大量互相调用的函数,你可以只导出你需要的那个主函数,其余的内部依赖函数一并复制过去。如果发生环境缺失报错,就根据报错信息逐一补充对应的全局变量。这个过程比较枯燥,但非常有效,十几轮下来,脚本基本就能跑出签名值了。
我印象最深的一次是,那个平台的前端代码里使用了一个自定义的字符映射表,把普通字符串里的每个字符都替换成了另外一套字符,然后才做 Base64 编码。如果只看最后生成的 wsgsig,完全想不到还有这一层映射。后来我是通过对比“相同输入、不同输出”的数据,发现字符频率分布异于常规 Base64,才意识到前置还有一层替换逻辑。
4.3 用本地脚本批量验证 wsgsig 参数
当本地脚本能生成一个与浏览器一模一样的 wsgsig 之后,接下来要做的就是批量验证。写一个循环,每次用不同的请求参数和时间戳调用签名函数,把结果同步发到接口上,观察接口是否正常返回数据。
如果接口返回正常,说明签名逻辑已经复现成功;如果返回参数错误或者签名错误,就要对比浏览器里的 wsgsig 和本地生成的 wsgsig 的差异点。常见的差异有三个位置:参数排序顺序不同、时间戳精度不同、字符大小写转换规则不同。逐个排查即可。
有一点需要明确,签名算法虽然是逆向分析的核心,但我强烈建议不要把这些技术用于恶意用途。合理的应用场景是:你拥有该平台账号,且在自己的权限范围内做数据分析和接口研究,或者你已经获得平台方的授权。否则,任何未经允许的批量调用都可能触碰法律红线。
5. 常见问题与排查技巧实录
5.1 参数能找到但一直签名失败
这是我被问得最多的问题。现象是:明明已经从浏览器里复制了 wsgsig,本地也定位到了加密函数,但生成的签名放到请求里就是失败。
排查优先级建议如下:
| 检查项 | 说明 |
|---|---|
| 时间戳精度 | 确认是毫秒级还是秒级,很多平台对时间戳窗口有 ±5 分钟限制 |
| 参数排序 | 确认参数对象是否按 ASCII 码升序排列 |
| 空值处理 | 确认空字符串、null、undefined 在拼接时如何被处理 |
| Cookie 关联 | 确认签名是否依赖某个 cookie 字段的值 |
| 环境指纹 | 确认是否读取了 Canvas、WebGL、字体等环境信息 |
这张表基本覆盖了绝大多数签名校验的隐藏条件。我遇到过最隐蔽的一次是参数里有一个值为 undefined 的字段,浏览器环境里 JSON.stringify 会直接丢弃它,但本地脚本里没有还原这个行为,导致拼接的字符串总是不一致。
5.2 搜索 wsgsig 关键字搜不到
有些平台为了防止逆向,会故意在压缩混淆时把参数名字符串打散。比如你搜索 wsgsig 而搜不到任何结果,但请求里明明带着这个参数。大概率是因为代码里是这样写的:
javascript复制const p = 'wsg' + 'sig';
或者更极端一点,参数名字符串被 Base64 编码后再动态解码。此时可以用 Network 面板右侧的 Initiator 调用栈从请求发起位置一路往回找,或者在全局检索里搜索 sig、sign 这类子串,通常也能找到线索。
另外,有些时候 wsgsig 的名字会在 webpack 打包时被当作普通字符串处理,分布在不同的 chunk 文件里。全文件搜索时注意切换搜索范围为 “content” 而不是 “file name”。
5.3 调试时发现签名函数被反复混淆
有些前端团队会同时使用多个混淆插件,比如将变量名替换为十六进制字符串、控制流扁平化、字符串编码等。这种代码直接阅读体验极差,但并不是没有应对方法。
我的经验是:不要试图完全读懂混淆后的代码,而是通过“行为观察法”来分析。具体来说,就是通过修改输入看输出差异,找出函数中处理数据的关键步骤。比如在浏览器的 Console 里手动调用加密函数,传入不同的时间戳,观察输出变化。或者用二分法:把加密函数拆成两段,分别测试中间结果的差异,快速锁定关键逻辑所在的位置。
5.4 本地生成的值比浏览器短很多
这个问题通常与编码方式有关。浏览器环境中 JS 字符串是 UTF-16 编码,而在进行 Base64 或者十六进制转换时,有些库默认按 UTF-8 字节处理。如果本地脚本直接处理一个包含中文参数的字符串,而浏览器里已经通过某种方式转换了编码,最后结果就会不一致。遇到这种情况,可以检查中间是否存在 encodeURIComponent、unescape 或 TextEncoder 的调用。补上对应的编码转换逻辑即可。
6. 写在最后的建议
网页参数分析这件事,本质上是一场“理解别人思路”的脑力活。wsgsig 这个名字本身并不神秘,它只是成千上万种前端签名方案中的一个缩影。掌握了定位、断点、调用栈分析、环境复现这套方法论,以后再遇到别的签名参数,基本就是换个名字、换个算法、换个拼接方式的事。
我自己在实际分析中最大的体会是:不要只盯着加密算法本身,很多坑都藏在算法之外。比如时间戳精度、cookie 关联、参数序列化方式、环境指纹,这些不起眼的细节才是决定成败的关键。把它们整理成一张检查清单,能帮你节省大量重复排查的时间。
最后再分享一个小技巧:分析时最好把所有尝试过的参数组合和结果记录成笔记,哪怕是很简单的修改也记下来。因为签名调试经常会出现“改了十几次,突然跑通了,却忘了上一次是否改对了”的情况。有了笔记,回退和对比都方便得多。
希望这篇东西能帮你在面对 wsgsig 之类的网页参数时少走一点弯路。如果你在分析时遇到了不一样的情况,欢迎带着具体现象和代码片段来交流,我们一起把问题拆开看。
