免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及小红书 Web 端(https://www.xiaohongshu.com)正常访问过程中的网络请求,仅用于安全研究、学习与了解 Web 前端请求保护机制。文中接口地址均来自浏览器正常访问产生的公开流量。请勿将本文技术用于任何未授权的系统,违者后果自负。
一、背景介绍
小红书 Web 端(edith.xiaohongshu.com)对 API 请求采用了一套前端签名保护机制,其中 x-s 是核心请求头,用于标识请求的合法性。本文以**首页信息流接口(homefeed)**为研究对象,分析 x-s 的生成机制。
核心签名相关请求头:
| 请求头 | 说明 |
|---|---|
x-s |
请求签名,每次请求动态生成,核心防护参数 |
x-t |
毫秒时间戳,参与签名计算 |
x-s-common |
设备/环境信息的 Base64 编码,固定值 |
x-b3-traceid |
链路追踪 ID,固定值 |
研究核心难点:
- 混淆 JS 的还原 (
02_source.js为高度混淆的字符串数组自解密结构) - Node.js 环境补环境 (原 JS 依赖浏览器
window、document、navigator等对象) x-s签名算法的定位与提取 (核心函数mnsv2)x-s-common的结构分析(自定义字母表 Base64 编码的设备指纹)
二、整体流程图
准备请求参数(url path + JSON body)
↓
计算辅助哈希:
f = url_path + json_body(字符串拼接)
c = MD5(f)
d = MD5(url_path)
↓
调用混淆 JS 中的 mnsv2(f, c, d) → 得到签名核心值 x3
↓
组装 x-s 结构体:
{"x0":"4.3.1","x1":"xhs-pc-web","x2":"Windows","x3":<签名>,"x4":"object"}
↓
对结构体 JSON 进行自定义字母表 Base64 编码
↓
x-s = "XYS_" + Base64结果
↓
携带 x-s、x-t、x-s-common 等请求头发起 POST 请求
三、抓包分析
3.1 目标接口
请求:
POST https://edith.xiaohongshu.com/api/sns/web/v1/homefeed
Content-Type: application/json;charset=UTF-8
关键请求头:
| 请求头 | 来源 | 说明 |
|---|---|---|
x-s |
本地签名生成 | 核心防护参数,见第四节 |
x-t |
本地时间戳 | 毫秒级时间戳 |
x-s-common |
固定值 | 设备指纹,Base64 编码的 JSON,详见第五节 |
x-b3-traceid |
固定值 | 链路追踪 ID |
xy-direction |
固定值 | 固定为 57 |
请求体示例(JSON):
json
{
"cursor_score": "",
"num": 15,
"refresh_type": 1,
"note_index": 15,
"category": "homefeed.cosmetics_v3",
"need_num": 8,
"image_formats": ["jpg", "webp", "avif"],
"need_filter_image": false
}
响应: 返回首页信息流 note 列表(JSON)。
四、x-s 签名生成算法
4.1 预处理:三个入参的计算
python
url_path = '/api/sns/web/v1/homefeed'
json_body = json.dumps(data, separators=(',', ':')) # 无空格紧凑格式
f = url_path + json_body # 路径与请求体的字符串拼接
c = MD5(f) # 对拼接结果做 MD5
d = MD5(url_path) # 对路径单独做 MD5
4.2 核心签名:mnsv2
混淆 JS(02_source.js)中注册了全局函数 mnsv2,挂载于 window 对象上:
javascript
// 混淆后原始形式(伪代码还原逻辑)
window.mnsv2 = function(f, c, d) {
// 内部通过字符串数组解密表(_0x5ae8)还原真实逻辑
// 核心:基于 f、c、d 三个输入,结合内置密钥流,生成签名字符串 x3
return x3 // 签名结果
}
注意 :
mnsv2的内部实现为字符串数组自解密混淆结构(见02_source.js前两行的_0x5ae8数组),不建议手动还原,直接在补环境后调用原始 JS 函数即可得到正确结果。
4.3 x-s 的组装与编码
得到 x3 后,按以下步骤组装最终 x-s 值:
Step 1:构造信息结构体
json
{
"x0": "4.3.1",
"x1": "xhs-pc-web",
"x2": "Windows",
"x3": "<mnsv2计算结果>",
"x4": "object"
}
| 字段 | 含义 |
|---|---|
x0 |
SDK 版本号 |
x1 |
应用标识 |
x2 |
操作系统 |
x3 |
核心签名值(mnsv2 输出) |
x4 |
固定字符串 "object" |
Step 2:自定义字母表 Base64 编码
小红书使用了非标准 Base64 字母表 ,与标准 A-Z a-z 0-9 +/ 不同:
小红书使用了非标准 Base64 字母表,具体字母表字符串需自行从 SDK JS 源码中提取(位于
03_main.js顶部的常量字符串定义处)。
编码流程:
- 对结构体 JSON 字符串进行
encodeURIComponent转义,提取各字节 UTF-8 编码 - 按标准 Base64 分组(每 3 字节 → 4 字符),但使用上述自定义字母表查表输出
Step 3:拼接前缀
x-s = "XYS_" + Base64编码结果
五、x-s-common 结构分析
x-s-common 是一个固定的长字符串,同样使用与 x-s 相同的自定义字母表 Base64 编码,解码后为设备指纹 JSON:
json
{
"s0": <平台标识>,
"s1": <...>,
"x0": <SDK版本>,
...
}
该值包含浏览器指纹信息,在会话期间相对固定。研究时可从浏览器抓包直接复用,无需重新生成。
六、Node.js 环境补全(补环境)
02_source.js 运行时依赖大量浏览器 API,在 Node.js 中直接执行会报错,需手动补充以下环境(见 01_env.js):
6.1 需补充的对象
| 对象/属性 | 补充方式 | 说明 |
|---|---|---|
window |
window = global |
将 Node.js global 作为 window |
self, top |
同上 | 同 window 引用 |
window.addEventListener |
空函数 | 阻止事件绑定报错 |
window.chrome |
空对象 {} |
模拟 Chrome 环境 |
document |
自定义 Document 类 |
补充 cookie、getElementsByTagName 等 |
document.cookie |
注入 cookie 字符串 | 部分签名逻辑会读取 cookie 参与计算 |
navigator.userAgent |
注入 UA 字符串 | 需与请求头中的 UA 保持一致 |
navigator.webdriver |
false |
规避 webdriver 检测 |
XMLHttpRequest |
空实现 | 阻止 JS 内部发起请求 |
MouseEvent、Screen 等 |
空函数 | 防止构造函数调用报错 |
6.2 补环境注意事项
document.cookie中注入的 cookie 需与 Python 请求中使用的 cookie 保持一致 ,因为签名函数内部可能读取a1、webId等 cookie 字段参与计算navigator.userAgent需与请求头user-agent一致- 补环境不完整时,建议用
Proxy对象拦截get/set操作,打印实际访问了哪些属性,按需补充
七、Python 调用 Node.js 方案
7.1 调用架构
Python (xhs.py)
└─ subprocess.run(['node', 'xhs/03_main.js', 'get_x_s', json_data])
└─ 03_main.js (入口)
├─ require('./01_env.js') # 补环境
└─ require('./02_source.js') # 混淆原始 JS
7.2 关键注意事项
路径问题(常见陷阱) :subprocess 调用 node 时的工作目录不一定是脚本所在目录,必须使用绝对路径:
python
import os
js_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "03_main.js")
result = subprocess.run(['node', js_path, function_name, arg], ...)
返回值解析 :03_main.js 通过 console.log 输出结果,Python 端读取 stdout;若 JS 内部有 debug 日志,需按换行符切分取目标行。
编码 :subprocess 调用时指定 encoding='utf-8',避免中文路径或中文字符导致乱码。
八、完整请求流程概述
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 构造请求体 | 按接口要求拼装 JSON,使用无空格紧凑格式 |
| 2 | 计算辅助哈希 | f = path+body,c = MD5(f),d = MD5(path) |
| 3 | 调用 mnsv2 | 通过 Node.js 执行补环境后的混淆 JS,传入 f/c/d |
| 4 | 组装 x-s | 构造结构体 JSON → 自定义字母表 Base64 → 加前缀 "XYS_" |
| 5 | 准备其他请求头 | x-t(时间戳)、x-s-common(设备指纹,可复用抓包值) |
| 6 | 发起 POST 请求 | edith.xiaohongshu.com,携带完整 headers 和 cookies |
| 7 | 解析响应 | JSON 格式,包含 note 列表等业务数据 |
九、关键知识点总结
| 知识点 | 详情 |
|---|---|
| 签名核心函数 | window.mnsv2(f, c, d),注册在混淆 JS 的全局作用域 |
| 入参构造 | path+body 拼接 → MD5 两次,得到 f/c/d 三个输入 |
| x-s 结构 | "XYS_" + CustomBase64(JSON{x0~x4}) |
| 自定义 Base64 | 字母表需自行从 SDK JS 源码提取,长度 64 字符,非标准 A-Za-z0-9+/ |
| x-s-common | 设备指纹,同编码方式,会话内固定,可抓包复用 |
| 混淆类型 | 字符串数组自解密(_0x5ae8 数组 + 自调用立即函数校验和) |
| 补环境关键点 | document.cookie 需与请求 cookie 一致,navigator.userAgent 需与 UA 一致 |
| 路径陷阱 | Python subprocess 调用 node 时需使用 __file__ 构造绝对路径 |
十、依赖安装
bash
pip install requests
node --version # 需要 Node.js 环境(建议 v16+)
本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。