简介:这是一份面向前端开发与网络安全爱好者的JavaScript逆向实战源码包,聚焦小红书X-s与X-T参数的生成逻辑分析,适合具备一定JS调试基础、希望理解网络应用加密机制的开发者学习参考。资源共3个文件,包含1个inscode工程配置、1个html页面与1个gitignore文件,压缩包约6KB,体量轻便,便于快速导入运行与调试。内容围绕抓包定位、Hook JSON.stringify断点、跟栈查找加密函数、补环境与RPC分析等环节展开,并借助插桩日志揭示MD5魔法值、Base64编码及URL、环境检测值、cookie中a1值、时间戳等关键参数,最终梳理出URL加参MD5、拼接x1至x4、Base64编码再加密生成payload的完整链路。目前已有121人学习,可作为理解前端加密与逆向流程的实践样本。
1. 从一次接口 403 说起:X-s 与 X-T 到底在防什么
做过前端数据采集的同行大概率遇到过这种场景:页面能正常打开,Network 面板里那条接口请求也看得见,可一旦把 URL 和参数原样复制到脚本里重放,服务端立刻回一个 403 或者干脆返回空数据。翻遍请求头,唯一多出来的就是X-s、X-T这两个自定义字段,值还是一串看不懂的密文。这就是本文要拆的东西——JS 逆向分析里针对小红书 Web 端签名参数 X-s / X-T 的还原思路,以及一份可运行的源码骨架。
它解决的核心问题只有一个:让脚本发出的请求,在服务端看来和浏览器里点出来的那次请求长得一模一样。适合谁?适合已经会写 Python 或 Node 请求、但卡在签名环节的采集开发者,也适合想系统学一遍 JS 逆向流程的前端同学。需要提前说清楚边界:签名算法会随前端发版变化,本文给的是方法论加可复现的工程结构,不是一劳永逸的固定密钥。真正值钱的是那套「定位—扣码—补环境—验证」的流程,换一个站点照样能用。
2. 定位签名生成入口:从请求头反查 JS 调用链
逆向的第一步永远不是读代码,而是找到「谁写了这个头」。很多人一上来就全局搜X-s,结果搜出几十个文件,直接懵掉。正确姿势是从浏览器运行时入手,让浏览器自己告诉你答案。
2.1 用 XHR 断点锁定发起请求的调用栈
打开开发者工具的 Sources 面板,在右侧找到 XHR/fetch Breakpoints,添加一个断点,URL 填接口路径的关键片段(比如/api/sns/web/v1/)。刷新页面触发请求,代码会停在发送请求的那一行。此时看 Call Stack,逐层往上翻,通常在三到五层之内就能看到一个函数,里面出现了X-s和X-T的赋值。这一步的价值在于:你拿到的是「真实执行路径」,而不是靠字符串搜索猜出来的可疑文件。
停住之后不要急着往下走,先在 Console 里打印当前作用域的关键变量,确认签名函数接收了哪些入参。常见入参包括请求 URL 的路径、查询串、请求体、时间戳、以及一个随机数。把这些记下来,后面扣代码时才知道要喂什么。
2.2 在混淆代码里识别签名函数的特征
前端代码基本都过了压缩和变量名混淆,函数名可能是a、b、_0x1a2b。但混淆改不掉结构特征,签名函数通常有几个明显标志:内部有大量位运算(^、<<、>>>)、有固定长度的常量数组、会调用Date.now()或performance.now()。定位到疑似函数后,在它的入口和出口各打一个断点,对比输入输出,就能确认它是不是真正的签名器。
如果函数被拆得很碎,可以顺着调用关系画一张简单的依赖图:谁调用了它、它又调用了哪些工具函数。这张图决定了你后面要扣多少代码。经验上,一个成熟的签名函数会依赖三到五层工具函数,包括 MD5、AES、Base64 变种和自定义的字符串拼接。
2.3 判断是纯算法还是带环境依赖
这一步直接决定工作量。在断点处观察签名函数内部是否访问了window、document、navigator、localStorage这类浏览器对象。如果只是纯数学运算加字符串处理,那恭喜你,扣出来丢进 Node 就能跑。如果它读了navigator.userAgent、屏幕尺寸、甚至canvas指纹,那就得在 Node 里补一套环境,工作量翻倍。
判断方法很直接:把函数体复制到一个干净的 Node 文件里,喂上断点处记录的入参,看报什么错。报window is not defined就补 window,报某个属性 undefined 就补那个属性。补环境是个体力活,但可控。
3. 扣代码与补环境:把浏览器里的签名逻辑搬到 Node
定位完成之后,核心工作是把签名逻辑从混淆代码里「抠」出来,让它脱离浏览器也能跑。这一步的成败,八成取决于补环境补得全不全。
3.1 最小化扣码:只保留签名链路
不要试图把整个 JS 文件搬进 Node,那里面全是页面渲染和埋点逻辑。正确做法是从签名函数入口开始,把它和它依赖的工具函数逐个复制到一个新文件,每复制一个就跑一次,报错就补,直到不报错为止。这个过程叫「最小化扣码」。
下面是一个扣码后的骨架示例,展示签名模块应该长什么样:
// sign.js —— 从混淆代码中抠出的签名模块骨架 const crypto = require('crypto'); // 工具函数:自定义的字符串混淆,实际项目中通常是变种 Base64 function encodeStr(input) { // 关键注释:这里的映射表来自扣码时提取的常量数组 const map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; let out = ''; for (let i = 0; i < input.length; i++) { out += map[input.charCodeAt(i) % map.length]; } return out; } // 核心签名函数:入参顺序必须和断点处观察到的一致 function buildSign(uri, data, timestamp, nonce) { // 关键注释:拼接顺序错了结果就全错,这是最容易翻车的地方 const raw = `${uri}|${data}|${timestamp}|${nonce}`; const md5 = crypto.createHash('md5').update(raw).digest('hex'); const xs = encodeStr(md5 + timestamp); // X-T 通常就是时间戳的某种变形 const xt = String(timestamp); return { 'X-s': xs, 'X-T': xt }; } module.exports = { buildSign };逻辑说明:buildSign接收四个参数,分别对应断点处观察到的 URI、请求体、时间戳和随机数。拼接顺序是签名算法的灵魂,顺序错了 MD5 结果完全不同。encodeStr模拟了原代码里的字符映射,实际扣码时这张映射表要从常量数组里原样搬过来。
参数说明:uri是接口路径不含域名;data是请求体的字符串形式,注意 JSON 序列化时键的顺序要和浏览器一致;timestamp用毫秒级;nonce是每次请求变化的随机值,通常由原代码里的随机函数生成。
3.2 补环境:让 Node 假装自己是浏览器
当签名函数依赖浏览器对象时,需要在 Node 里造一个假的全局环境。常见做法是在文件顶部定义一个window对象,把用到的属性都挂上去。
// env.js —— 补环境脚本 global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', platform: 'Win32', language: 'zh-CN' }; global.document = { cookie: '', createElement: () => ({ getContext: () => ({}) }) }; global.location = { href: 'https://example.com/' }; // 关键注释:有些签名会读 performance.now,补一个递增的假值 let _t = Date.now(); global.performance = { now: () => (_t += 1) };逻辑说明:把global直接赋给window,是为了让代码里window.xxx和xxx两种写法都能取到值。navigator里的字段要尽量贴近真实浏览器,因为部分站点会校验 UA 和平台的一致性。
参数说明:userAgent建议和你实际请求时带的 UA 保持一致,否则可能出现「签名过了但接口仍拒绝」的玄学问题。performance.now补成递增值,避免某些防重放逻辑判定异常。
3.3 用真实请求验证签名正确性
扣完代码别急着上量,先用一条真实请求验证。把浏览器里那次成功请求的 URL、参数、时间戳记下来,用你的签名函数重新算一遍,对比结果是否一致。一致说明算法对了,不一致就回到断点处逐行比对中间变量。
# verify.py —— 用 Python 调用 Node 签名并验证 import subprocess, json, time, requests def get_sign(uri, data): ts = int(time.time() * 1000) payload = json.dumps({"uri": uri, "data": data, "ts": ts}) # 关键注释:通过子进程调用 Node,避免用 Python 重写一遍算法 out = subprocess.check_output(['node', 'sign_cli.js', payload]) return json.loads(out) uri = '/api/sns/web/v1/feed' data = '{"cursor_score":"","num":20}' sign = get_sign(uri, data) headers = { 'X-s': sign['X-s'], 'X-T': sign['X-T'], 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } resp = requests.post('https://example.com' + uri, data=data, headers=headers) print(resp.status_code, resp.text[:200])逻辑说明:用子进程调用 Node 是最省事的方案,不用把 JS 算法翻译成 Python。sign_cli.js负责读参数、调buildSign、输出 JSON。
参数说明:ts用毫秒时间戳,和签名函数内部用的必须同源。请求头里的 UA 要和补环境时的一致,这是很多新手忽略的细节。
4. 避坑与排查:签名过了接口还是拒绝的五个原因
签名算对了不等于请求能通,下面这几条是我踩过的血泪经验,按「现象 → 原因 → 解决」列出来。
现象一:本地算出的 X-s 和浏览器不一致。原因通常是拼接顺序或分隔符错了,也可能是某个入参在浏览器里被 URL 编码过而你没编码。解决:在断点处把每个中间变量打印出来,和本地逐一对齐,重点看分隔符是|还是&,以及有没有做encodeURIComponent。
现象二:签名一致但接口返回 403。原因多半是请求头不全,服务端除了校验签名还校验Referer、Origin、Cookie里的某个字段。解决:把浏览器成功请求的完整请求头复制过来,逐个删减测试,找出那个必需的字段。
现象三:跑几十条后开始失败。原因是触发了频率限制或风控,和签名无关。解决:加请求间隔、控制并发、复用会话,别用同一个 nonce 反复请求。
现象四:换台机器签名就失效。原因是签名里混入了设备指纹,比如屏幕分辨率或 canvas 哈希。解决:回到补环境那一步,把指纹相关字段固定成常量。
现象五:Node 里跑正常,打包后报错。原因是某些全局对象在打包环境里被改写。解决:把补环境代码放在最前面执行,确保在任何业务代码之前生效。
提示:签名算法会随前端发版变化,建议把扣码过程写成文档,下次发版时按图索骥,比重新逆向快得多。
5. 工程化收尾:把签名模块做成可维护的独立服务
单文件脚本能跑通验证,但真要长期用,得把它工程化。我一般会把签名逻辑抽成一个独立的 HTTP 服务,主采集程序通过本地接口调用它。这样做的好处是:签名算法更新时只改一个地方,采集程序不用动;Node 和 Python 的边界清晰,调试也方便。
// server.js —— 签名服务,暴露本地 HTTP 接口 const http = require('http'); const { buildSign } = require('./sign'); require('./env'); // 关键注释:补环境必须在最前面引入 http.createServer((req, res) => { let body = ''; req.on('data', c => (body += c)); req.on('end', () => { const { uri, data, ts, nonce } = JSON.parse(body); const sign = buildSign(uri, data, ts, nonce); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(sign)); }); }).listen(3000, () => console.log('sign service on 3000'));逻辑说明:服务只做一件事——收参数、算签名、返回结果。env必须在sign之前引入,否则补环境不生效。
参数说明:端口选一个不冲突的本地端口即可;nonce由调用方生成并传入,保证每次请求唯一。
进阶技巧上,我习惯在签名服务里加一层缓存和日志:把每次的入参和结果记下来,一旦接口开始失败,翻日志就能判断是算法变了还是风控升级了。另外,定期用浏览器手动请求一次,把结果和服务的输出做比对,相当于给自己留了一颗后悔药。这套流程我用了很久,最大的教训是:别把签名算法硬编码进采集主逻辑,否则前端一发版,你就得满世界找哪里写死了。希望帮到你。
本文还有配套的精品资源,点击获取