
简介一份聚焦小红书X-s/X-T参数生成逻辑的JavaScript逆向工程可运行源码包面向JS逆向初学者和有前端加密分析需求的开发者尤其适合希望通过真实站点练习抓包、Hook、插桩和补环境技术的读者。压缩包内共3个文件以HTML页面、inscode运行配置和gitignore辅助文件为主整体仅6KB结构精简、无多余依赖便于快速启动和对照学习。已有99人学习下载适合作为Web安全与前端加密技术的入门案例。源码中保留了关键加密流程的日志插桩信息可帮助读者逐步还原URL参数MD5、拼接x1-x4并Base64编码后生成payload的完整链条同时提供从断点调试到RPC分析的排错思路。整体分析方法不限于小红书也能迁移到其他Web应用为理解前端签名参数生成机制提供直观参考。 做 JS 逆向的同行应该都有这种经历别人发来一个数据接口你兴冲冲把 URL 复制到 Postman 里点发送返回的不是 JSON而是一段验证页面的 HTML或者一句“请求参数不合法”。小红书的 X-s/X-T 就是这类风控里最典型的一组签名参数。小红书 Web 端几乎每个业务接口——笔记详情、搜索接口、用户主页、图片列表——都要求请求头带上这两个值缺一个都进不去。这篇内容主要梳理我在分析小红书 X-s/X-T 过程中的完整思路这个签名到底是干什么的、怎么定位生成逻辑、从 JS 里提取算法时有哪些坑、落地成 Python 代码时怎么保证“能跑”以及真正工程化时还要注意哪些问题。适合刚接触 JS 逆向、想拿真实站点练手的人也适合已经被 X-s 签名校验卡住、想找排查思路的人。1. X-s/X-T是干什么的一次不带签名的请求长什么样先说 X-T。这个参数相对简单它就是当前请求时间的毫秒级时间戳。你随便在浏览器里看一个笔记接口的请求头X-T 和当前时间基本只差几毫秒。这个参数的作用是告诉服务端“这次请求是新鲜的”同时也参与后面的签名计算防止有人把旧请求拿去重放。X-s 才是核心。它是一串 64 位的十六进制字符串长度和 MD5 输出一致但实际生成过程并不是简单地对 URL 做一次 MD5。它服务端要校验的是“这次请求携带的所有参数有没有被篡改过”——URL 里的查询参数、请求体、X-T 时间戳、当前请求路径都会被打包进签名算法里。只要你在原始参数里动了一个字符X-s 就得重新算直接用上次的 X-s 去请求另一个参数组合签名必然对不上。所以这套校验的本质其实和很多支付平台、开放平台的 appKeysign 机制是一个道理用一串只有“客户端知道规则”的哈希值证明这次请求确实来自真实客户端。要绕过它不是把 X-s 删掉或者随便填一串 MD5 就行而是得搞清楚算法到底对哪些输入做了哈希、以什么顺序做哈希、中间有没有加盐salt。动手验证之前建议先在浏览器无痕窗口打开小红书网页版随便点开一篇笔记然后在 Network 面板里找到api相关的请求。你会看到请求头里带着x-s、x-t还有x-s-common、x-b3-traceid之类的一堆参数。最初我只盯着 X-s 看后来才明白 X-s-common 里也塞了不少设备指纹信息触发风控的时候服务端是多个维度联合判断的。当然第一关还是 X-s它过不去就别谈后面的。2. 签名算法的逆向推演从浏览器到源码的定位路径2.1 用启动器在调用栈里找入口打开开发者工具切到 Sources 面板在左侧文件列表里全局搜索x-s注意是带连字符的一般能搜出几处代码其中会出现类似这样的赋值逻辑n.headers[x-s] (0, r.default)(...)在那一行打上断点然后刷新页面或重新触发一次请求等断点命中后右侧 Call Stack 会展示从请求发起到签名生成的整个调用链。核心思路就是沿着调用栈逐层往外走直到看到一处“拿到了所有参数并开始做字符串拼接”的函数。我踩过最大的弯路是想直接在代码里搜“MD5”关键词以为能一步定位到算法。但实际代码里的 md5 工具函数可能是按模块打包的名字五花八门比如C.default、s.a直接搜关键词会搜出一堆无用结果。反而从浏览器发起请求的真实调用栈往回追更可靠。断点命中后关注栈里每一层的函数名反复跑几次请求记录每次都会经过哪些函数那些稳定出现的函数就是签名的必经之路。2.2 还原签名计算的输入顺序定位到核心函数之后关键问题是它对哪些输入做了哈希我见过的简化模型大致是这样注意具体变量名和盐值每过一段时间就会变这里只讲方法论function generateX-s(params, path, x_t) { // 第一步把所有 query 参数按 key 排序 // 第二步拼接成 keyvaluekeyvalue 的形式 // 第三步把 path 和 x_t 也拼进去 // 第四步加上一段写死在 JS 里的字符串 // 第五步做一次 MD5得到 X-s }所以你会看到 X-s 的输入至少包含三个部分排序后的查询参数、请求路径、时间戳。每一部分缺了或顺序不对签名都校验失败。这也是为什么你不能拿别人的 X-s 来直接发自己的请求——你们的请求参数组合可能不一样。这里有个很重要的取巧手段不需要你完全看懂每一行混淆代码。你只要能在生成 X-s 的函数入口处下一个条件断点然后分别构造不同的请求参数观察函数输入值的变化规律就能推断出拼接格式。比如你只改动 query 里的一个 id 值断点处显示的参数字符串里对应位置变化了那说明这个 id 被纳入了签名计算如果你连续两次带不同的请求体字符串没变化说明这次算法没把请求体算进去。这种“黑盒观察法”在分析压缩混淆过的 JS 时特别高效。3. 可运行的参考实现execjs 桥接与纯 Python 重写3.1 最省事的方案execjs 直接调用 JS 原函数如果你只是想做数据分析、偶尔拉一次数据没必要把 JS 算法完整重写成 Python。把包含签名算法的那一段 JS 代码提取出来用execjs在 Python 里调用这是最快的落地方式。核心逻辑只有三步把算法源码抽到一个临时 JS 文件里在execjs.compile时加载它然后调用你定位到的那个生成函数。下面是简化示例目的是演示调用方式实际使用时要替换成你自己定位到的函数名和参数// sign.js function getXs(query, path, x_t) { // 这里的实现只是占位实际项目中请从真实页面提取 var sorted Object.keys(query).sort(); var raw ; for (var i 0; i sorted.length; i) { raw sorted[i] query[sorted[i]] ; } raw raw.slice(0, -1) path x_t salt; return md5(raw); }import execjs with open(sign.js, r, encodingutf-8) as f: ctx execjs.compile(f.read()) xs ctx.call(getXs, {id: 123}, /api/note/detail, 1710000000000) print(xs)用 execjs 最大的好处是省事你不需要完全理解算法内部每一行。但它的缺点是性能一般每次调用都要起一个 JS 运行时如果你需要高频请求或者并发请求这里会成为瓶颈。另一个风险是算法文件更新后你得重新提取维护成本偏高。3.2 纯 Python 重写时的关键细节如果你想要长期稳定运行最终还是得把算法逻辑翻译成 Python。就我们刚才说的简化模型翻译出来无非是排序、拼接、哈希import hashlib import time def generate_xs(query: dict, path: str, x_t: str) - str: sorted_keys sorted(query.keys()) raw_parts [f{k}{query[k]} for k in sorted_keys] raw .join(raw_parts) path x_t # 盐值部分需要根据实际 JS 提取结果调整 raw your_salt_here return hashlib.md5(raw.encode()).hexdigest()看着简单但真跑起来会遇到不少细节问题。第一是大小写问题MD5 输出的十六进制字符串在 JS 里可能经过了toUpperCase()你直接比较前先统一大小写。第二是空值处理query 里如果某个参数是空数组或者undefinedJS 端的序列化方式和 Python 里str({})完全不同这部分最容易造成签名对不上。第三是 URL 编码差异有些参数值里有中文或特殊字符JS 端拿到的是解码后的值还是编码后的值直接决定了拼接结果。我个人的建议是先用 execjs 跑通全流程拿到真实的 X-s 值作为“标准答案”再用 Python 逐步复现同一个请求的签名结果对着比较。两边算出来的值一致了再考虑把 execjs 替换成纯 Python 版本。这样能省掉大量盲猜时间。4. 签名正确性的判断与几个隐蔽的坑4.1 怎么确认你的 X-s 真的算对了签名对不对最直观的验证方法就是发起一个真实请求看返回结果。签名正确时返回正常的 JSON 数据签名错误时通常会返回一个包含validate或captcha关键词的响应有些情况下甚至直接返回一个空页面。更隐蔽的情况是签名算法本身是对的但因为你某个参数的值在计算时和实际请求时不一致导致请求时前端用 A 值签名、服务端拿 B 值校验最后被判定为非法。所以自测时要顺手做一个“一致性校验”从浏览器复制一个真实请求的完整信息——URL、请求头、请求体、X-s、X-t——然后用你自己生成的 X-s 去替换请求头里的值X-t 保持不变其他参数也保持不变看是否还能拿到同样的响应。如果替换后立刻失败说明你的生成算法和真实算法还有差异如果替换后成功说明算法已匹配。4.2 时间窗口、参数顺序和编码问题后边这些坑是我实际踩过的逐条说清楚。第一是时间戳精度。X-t 必须是毫秒很多时候你如果用time.time()直接转字符串拿到的是带有小数点的浮点数拼进签名的字符串和实际请求头里发的值不一样签名肯定错。正确做法是str(int(time.time() * 1000))。第二是参数排序的隐藏规则。排序不一定是最简单的按 key 字典序有的服务端会用 ASCII 码排序有的则希望保持参数在 URL 中原本出现的顺序。你需要从上一节说的断点观察法里确定真实的排序方式不要理所当然地以为sorted(query.keys())一定正确。第三是 URL 编码差异。假设你的 URL 里有query你好浏览器在发请求时可能把它编码成%E4%BD%A0%E5%A5%BD但 X-s 计算时使用的可能是解码后的你好。如果你在 Python 里用quote()后又拼进签名会得到完全不同的哈希。这个坑很隐蔽因为报错信息不会告诉你究竟是哪一步出问题只能通过反复替换请求参数来定位。第四是路径本身。同一个接口可能同时存在多个域名前缀但签名里用的 path 是域名后面的那一部分。注意不要把https://www.xiaohongshu.com也并进 path 里多并一个域名和不并域名算出来的签名字符串是完全不同的。5. 别只盯着签名签名之外的风控链路X-s/X-T 只是第一道门槛它负责确认“请求参数有没有被篡改”。但小红书的风控显然不是只靠这一个签名你在请求头里还会看到 x-s-common这是一个很长的 JSON 序列化字符串里面包含了浏览器指纹、设备信息、屏幕参数甚至一些历史行为特征。服务端大概率会让签名通过之后再进入第二层行为校验访问频次、请求间隔、IP 归属、账号状态这些都会影响最终结果。从工程化的角度把 X-s 生成稳定之后你面临的常见问题还包括请求频率稍微快一点就可能触发验证码同一个 IP 大量请求会被限制访问没有登录态的匿名请求很多接口拿不到完整数据。所以其实很多人会搭配代理池、限速和合理的 User-Agent 来降低触发风控的概率。但这已超出“JS 逆向”本身属于反爬对抗的另一个范畴了。这里必须提醒一句合规边界分析 X-s/X-T 的生成逻辑、破解签名算法这类行为适合用于学习前端安全、研究接口加密方式或者在你拥有合法授权的前提下做技术验证。公开大规模抓取平台数据可能违反网站服务条款尤其涉及用户隐私数据时需要格外谨慎。建议做技术研究时控制请求量不要给目标服务器造成压力更不要将抓取数据用于商业用途。6. 几条压箱底的经验这套流程走下来我的体会是逆向 X-s 这类签名最重要的不是拿到最后那行生成代码而是掌握一套“定位并验证”的方法。签名算法隔一段时间就会更新你这次提取出来的 salt 可能下个月就失效了但调试验证的框架可以一直复用。再分享两个小技巧。第一在断点调试时不要急着看整个函数的代码先看函数被调用时接收了哪几个参数再用 console 把这些参数的字符串形式打印出来理解参数结构比看懂混淆代码更重要。第二如果你发现某次请求的 X-s 算法无论如何都和你本地算的不一致别怀疑自己的代码先去看服务端是不是下发了一套新的 JS 文件到浏览器里。前端 JS 文件更新有种方式是带版本号的Network 面板里按文件大小排序看一眼那些体积特别大的 vendor 文件往往是算法藏身之处。最后说一句实在话这类签名分析技术本身是中性工具你拿它来做研究、做辅助调试、做个人数据备份都行但批量采集和数据滥用一定要守住底线。希望这篇能把你在 X-s/X-T 上的弯路省掉一部分。本文还有配套的精品资源点击获取