4-小红书签名体系与风控实战(2026-10 实测)

实测时间:2026-10-03/04 | webBuild 6.56.2 | Chrome 154 | 项目:Z:\学习资源和笔记\Python爬虫全栈学习\项目\小红书
老版本资料(MediaCrawler 时代)的 window._webmsxyw 全局签名 + edith 单域 + v1 接口已部分过时,本文以实测为准。

一、风控全景:五层体系

┌─ 签名层 ── X-s/X-t(两代并存)+ X-S-Common(设备指纹包)+ traceid 双头
├─ 令牌层 ── xsec_token(服务端逐笔记签发,feed/评论必须带,上游响应传递)
├─ 会话层 ── web_session(HttpOnly,服务端主动轮换)+ SSK 密钥交换(X25519)
├─ 打标层 ── 461(设备/流量异常,浏览器同 cookie 正常 = 环境维度打标)
└─ 处置层 ── 300011 Account abnormal(账号级,全局止损)

域名分流(2026-10 实测)

域名 用途
so.xiaohongshu.com 搜索(v2 接口独占此域)
edith.xiaohongshu.com 其余业务 API(feed/评论/登录/用户)
as.xiaohongshu.com 安全脚本(ds)
fe-static.xhscdn.com 静态资源(vendor/ace 签名脚本)
t2.xiaohongshu.com / apm-fe 埋点/监控(不模拟,未见因此触发风控)

二、签名层:两代 X-s 并存(关键发现)

2.1 浏览器在用的:XYS_ v1(seccore_signv2)

vendor-dynamic.js 里的组装链(源码冻结在 analysis/js_frozen/):

seccore_signv2(uri, data):
payloadStr = uri + (isObject(data) ? JSON.stringify(data) : data)
x3 = window.mnsv2(payloadStr, md5(payloadStr), md5(uri)) // ds.js JSVMP,输出 mns0301_ 前缀串
S = {x0:平台码, x1:"xhs-pc-web", x2:"PC", x3, x4:typeof data, x5:md5(payloadStr)}
R = buildEncSskSign(md5hex) // SSK 签名(x6/x7)
X-s = "XYS_" + 自定义b64(encodeUtf8(JSON.stringify(S)))
  • mnsv2 由 as.xiaohongshu.com/api/sec/v1/ds?appId=xhs-pc-web(OB 混淆 JSVMP,59.8KB)动态挂载,
    源码中搜不到字符串(运行时拼属性名)。Node 补环境只挂 _dsf/_dsn/_dsl,mnsv2 挂载需更完整 DOM——未深挖。
  • 工具函数(webpack 模块 384):Pu=MD5、lz=encodeUtf8(encodeURIComponent 后按字节展开)、
    tb=CRC32 变体(多项式 0xedb88320,-1^c^poly)、xE=自定义字母表 base64
    (字母表 ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5,与 MediaCrawler 一致未变)。

2.2 我们产出的:XYW_ v2(ace.js)——部分接口接受

fe-static.xhscdn.com/as/v1/f218/a15/public/04b29480233f4def5c875875b6bdc3b1.js(148KB,_ace_ 系 VMP)
挂载 window._webmsxyw(uri, data),返回:

{"X-s": "XYW_" + 标准base64({"signSvn":"56","signType":"x2","appId":"xhs-pc-web","signVersion":"1","payload":"<416hex>"}),
"X-t": <ms时间戳>}

实测分接口(2026-10-03/04):评论 GET 某时段收 code=0;feed/search/login 拒(461/-1)——
v2 是灰度签名,不能全局依赖。

2.3 XYS_ v1 的落地:浏览器 oracle + Python 组装(主通道,全接口 code=0)

mnsv2 由 vendor-dynamic.js 内嵌 288KB JSVMP(signV2Init() eval 执行,webpack 模块 4455)挂载——
Node 补环境执行挂载失败于 VM 环境分流(_AUuXfEG27Xa3x 解释器黑盒,0 次挂载尝试)。
落地架构(Boss stoken 同款供给思路):

Python 爬虫 ──POST(uri,data)──> 本地队列 127.0.0.1:17777(mns_oracle.py serve)
↑ task {c, md5c, md5u} ↓ result {x3}
浏览器 worker(mainWorld 注入,300ms 轮询)──> window.mnsv2(c, md5c, md5u)
Python 侧组装:md5(本地) + x3(oracle) + SSK sha1链(本地) + 自定义b64(本地) → XYS_
  • md5/payload/SSK(x6/x7)/base64 全部本地纯算,mnsv2 是唯一黑盒走 oracle
  • worker fetch localhost 需 CORS 头 Access-Control-Allow-Private-Network: true(Chrome PNA)
  • 浏览器导航清 worker(页面级注入),签名页需保持不动
  • XYS_ 内部结构(解包实测):{x0:"4.4.3"(SDK版本), x1:"xhs-pc-web", x2:"Windows", x3:<mns0301_串>, x4:typeof data, x5:md5(payload)},登录后附加 x6/x7(SSK)
  • 时效宽松:实测 35 分钟前的签名仍可重放(绑 uri+body 不可跨请求复用)

2.4 补环境要点(Node 24)

  • 脚本尾部自带 Node 兼容(glb = typeof window === 'undefined' ? global : window),最小环境即可跑通
  • Node 21+ 的 navigator 是只读 getter,需 Object.defineProperty(globalThis, 'navigator', {...}) 覆盖
  • 最大坑:POST body 必须以对象传给 _webmsxyw(浏览器 axios 拦截器传的是对象,VM 内部自行
    JSON.stringify);传字符串会走不同分支 → 签名与 body 不匹配 → 406
  • 对拍验证:浏览器与 Node 同输入产出 payload 逐字节一致(同 208 字节、同 head hex)——环境等价实证

2.5 X-S-Common 结构全解(可本地生成)

解码(自定义 b64 逆 + unquote)后:

{"s0":5, "s1":"",
"x0":"1", // zIndex
"x1":"4.4.3", // 签名SDK版本
"x2":"Windows", // 平台(s0=5 同义)
"x3":"xhs-pc-web",
"x4":"6.56.2", // webBuild
"x5":"<a1 cookie>",
"x6":"","x7":"",
"x8":"<指纹>", // 三形态:~100字符不成熟 b1 / 1600 成熟 b1 / 1928 miniUa
"x9":<全长CRC32变体(x8)>,
"x10":<签名计数器,sessionStorage>,
"x11":"normal",
"x12":"<当前ms>;<dsl>"}

x9 的大坑(本轮 406 根因之一):ed.tb 是全长 CRC32 变体(多项式 0xedb88320、
-1^c^poly、JS 32 位有符号可为负)——不是 MediaCrawler 老版 mrc 的”前 57 字符截断”
(那是另一处 x9)。实测对拍:全长版与浏览器逐位一致(-587810826),截断版必错。

x8 指纹三形态:初始 ~100 字符(不成熟)→ 页面运行后 1600 字符(成熟 b1)→ 特定 URL
1928 字符(FingerprintV3 miniUa)。b1 前缀由环境决定(同机同前缀),但换 a1 必须整套换档案
(新 a1 配旧 x8 = 服务端识破 code:-1)。

2.5 SSK 密钥交换(2026 新增)

  • 登录 qrcode/status 请求带 client_public_key_base64(X25519 公钥)
  • 服务端返回加密 SSK → 前端 X25519 共享密钥 + secretbox(XSalsa20-Poly1305)解密 → localStorage webSsk
  • 每个签名算 x6/x7 = sha1 组合(encSskSign/encSsk,逐 appId)
  • 补环境直接从浏览器导出 webSsk 复用即可(device.json),无需重放交换

三、数据接口(2026-10 实测)

接口 方法/域 关键参数 响应要点
搜索 POST so./api/sns/web/v2/search/notes keyword/page/page_size/search_id/sort/note_type/image_formats/session_id items[].note_card(display_title/interact_info/cover/xsec_token/corner_tag_info)
详情 POST edith./api/sns/web/v1/feed source_note_id/xsec_token/xsec_source/extra.need_body_topic note_card(title/desc/time/tag_list/image_list/video.stream/interact_info/ip_location)
评论 GET edith./api/sns/web/v2/comment/page note_id/cursor/xsec_token comments[](content/like_count/create_time/user_info)+ has_more/cursor;子评论 sub/page
用户笔记 GET edith./api/sns/web/v1/user_posted user_id/num/cursor notes[](note_id/xsec_token/display_title)

search_id 来源:v2 响应不返回;它由搜索页 SSR HTML 注入(__INITIAL_STATE__.search.searchContext.searchId)——
爬虫须先 GET /search_result?keyword= 页面提取(顺带 cookie 预热),同关键词翻页复用。

xsec_token 机制:服务端逐笔记签发的访问令牌,从搜索/用户列表响应里带出,feed/评论请求必须回传——
纯构造 note_id 直调 feed 会被拒(-104 类)。

四、风控实证(脚本流量视角)

信号 语义 应对
406 + code:-1 签名/头与请求不匹配 修签名(对象传参);X-S-Common 缺失在浏览器侧表现为 -104
461 + code:0 空data 触发 redcaptcha 验证挑战(verifyBiz=461,见下) 扫码解锁(redcaptcha 流程);勿连发重试
471 验证流程瞬断(浏览器实测自动重试恢复) 退避重试
300011 账号异常(伴随 APP 被踢/短信警告) 全局止损;真人 APP 重登 + 浏览器扫码降级
-100 web_session 轮换/失效 吸收 Set-Cookie / 重登
-104 签名与 cookie 会话不一致 / 缺 x-s-common 对齐后重试

461 的真实语义:redcaptcha 验证挑战(重要修正)

设备指纹突变(清 cookie)触发浏览器 302 到:

/website-login/captcha?verifyUuid=<uuid>&verifyType=124&verifyBiz=461
  • verifyType=124 = APP 扫码身份确认(redcaptcha 自研,非滑块/点选)
  • 协议:POST /api/redcaptcha/v2/qr/init(verifyUuid→rid 渲染二维码)→ APP 扫码确认 →
    POST /api/redcaptcha/v2/qr/status/query 轮询 → 461 解除
  • 对抗哲学:身份确认型验证码,机器不可纯算破解——只能减少触发(环境做真)+ 扫码解锁

环境注册:shield/webprofile(真人化关键差异)

真人浏览器每次页面导航都上报 POST as.xiaohongshu.com/api/sec/v1/shield/webprofile:

{"platform":"Windows","sdkVersion":"4.3.5","svn":"2","profileData":"<加密hex ~11KB>"}

服务端凭 profileData 维护 a1↔环境映射。纯 API 脚本从不上报 = 环境档案缺失/突变 =
异常评分
。应对:会话启动时重放导出的 webprofile(http_client.warm_profile 已实现)。

打标是累积的(本轮实证时间线)

几分钟内十几条脚本请求(其中约一半是无效签名试探 + 过期 cookie 的必败请求)→ 461 验证挑战
持续 → 升级 300011(账号异常 + APP 被踢 + 风控短信)。无效请求也在烧信用——先本地对拍再上服务端。
461 期间浏览器同 cookie 正常(挑战只挂在脚本流量维度)。

五、可迁移方法论

  1. 新旧签名并存时先测新版接受度:XYW_ v2 服务端直接收,省掉 mnsv2 JSVMP 按天级的还原预算。
  2. 补环境对拍标准:同输入 payload 逐字节一致 = 环境等价(本轮 208 字节 hex head 全同)。
  3. SSR 页面是隐藏参数源:search_id 这类”响应不返回、请求必带”的参数,先翻 __INITIAL_STATE__。
  4. 同名脚本的版本路由:04b2...js 在 as/v1/3e44/ 和 as/v1/f218/a15/ 两路径下内容不同——
    扣脚本认准实际加载的 URL(hash 校验)。
  5. 风控信号要先分层再处置:461(环境打标)/ 300011(账号处置)/ -100(会话轮换)三者解法完全不同。
  6. 止损规则代码化(Boss 沉淀):单请求最多刷 2 次签名、461 连续 2 次全局停、300011 直接终止。

refer

  • 项目:Z:\学习资源和笔记\Python爬虫全栈学习\项目\小红书(analysis/ 有全部抓包样本与 JS 冻结)
  • MediaCrawler(x-s 自定义 b64/CRC 对照样本):https://github.com/NanmiCoder/MediaCrawler
  • 关联:《2-企查查反爬实战》《3-Boss直聘安全网关》《补环境对拍方法论》《浏览器指纹伪造方案》

声明:

  1. 若文章存在错误,望诸君不吝指正^
  2. blog仅供个人记录学习所用
  3. 部分笔记由于年代久远,做的笔记找不到最初是引用谁的,若是不允许引用转载,请联系我