1. 项目概述小红书x-s签名算法的逆向工程实战最近在搞小红书数据相关的项目发现它的接口防护又升级了特别是那个x-s签名简直是爬虫和自动化工具的头号拦路虎。我花了差不多一周时间从抓包分析到算法还原总算把2024年9月这个最新版本的x-s、x-s-common、x-rap-param这几个核心请求头的生成逻辑给摸透了。这玩意儿本质上是一个基于请求路径和请求体Body的加密签名算法无论是GET还是POST请求都得用它目的就是为了防止非法的接口调用和数据爬取。如果你也遇到过调用小红书API时返回403 Forbidden或者签名错误那这篇文章就是为你准备的。我会从最基础的抓包开始带你一步步拆解整个签名算法的构成包括如何定位关键加密函数、如何还原核心的加密逻辑最后还会分享一个可以直接拿来用的Python实现库。整个过程不涉及任何客户端模拟或者RPC调用是纯粹的算法还原Pure Calculation这意味着你可以在服务器端、任何编程语言中复现这个签名稳定性极高。2. 逆向工程的核心思路与准备工作2.1 为什么选择“纯算”而非RPC或模拟在逆向一个App的接口时通常有几条路可以走一是使用frida、xposed等框架进行RPCRemote Procedure Call调用直接调用App内部的加密函数二是使用playwright、appium等工具进行完整的UI或协议模拟三就是我们这次要做的“算法还原”。RPC调用看起来最省事直接从内存里捞结果。但它有几个致命缺点一是严重依赖特定的App版本小红书一更新你就得重新找偏移地址维护成本高二是需要在移动设备环境或模拟器中运行难以集成到服务端的自动化流程里三是存在法律和安全风险。模拟的方式则太重了资源消耗大效率低。而算法还原就是通过静态分析看代码和动态调试看执行过程把黑盒的加密过程变成白盒的可计算公式。一旦还原成功你就可以用任何语言、在任何地方生成完全合法的签名。它的好处是稳定、高效、可移植缺点是前期分析过程比较烧脑。但对于小红书这种核心业务接口算法在短期内不会频繁变动投入一次是值得的。2.2 必要的工具与环境搭建工欲善其事必先利其器。逆向分析需要一套组合工具抓包工具这是所有分析的起点。推荐使用Charles或Fiddler Everywhere它们对HTTPS流量的解密和重放支持得很好。关键是要在你的测试手机和电脑上安装好CA证书并开启SSL代理。反编译与调试工具对于Android应用Jadx-GUI是反编译APK查看Java代码的神器。对于Web端或小程序浏览器开发者工具F12中的Sources面板和Debugger就是主战场。如果遇到Vue或React打包的代码可能需要sourcemap或者借助AST解析工具来还原。JavaScript调试环境因为签名算法大概率在前端JavaScript中执行。一个无头浏览器环境如Puppeteer或Playwright可以用来动态加载页面并注入调试代码。更直接的方式是使用Node.js配合vm2模块来安全地执行和调试提取出来的JS代码片段。编程语言分析过程中用Python写辅助脚本非常方便比如自动化的参数对比、算法验证等。最终算法还原后你也可以用Python、Golang、Java等任何语言来实现。我的分析环境是macOS系统使用Charles抓包针对小红书Web端edith.xiaohongshu.com进行分析。选择Web端是因为其JavaScript代码虽然经过混淆但相对于移动端Native代码动态调试的门槛还是低一些。注意所有分析请务必基于你自己有权限访问的账号和数据严格遵守robots.txt协议仅用于学习和技术交流目的避免高频请求对目标服务器造成压力。3. 签名算法关键字段的抓取与分析3.1 初识小红书接口的签名Headers首先打开Charles设置好代理然后在浏览器中正常访问小红书官网并登录。随意点击几个笔记刷新一下主页你会在Charles中看到大量向edith.xiaohongshu.com这个域名发起的请求。我们找一个典型的接口看看比如获取用户发布列表的GET https://edith.xiaohongshu.com/api/sns/web/v1/user_posted?num30cursoruser_idxxx。查看这个请求的Request Headers你会发现一堆以x-开头的自定义头它们就是我们的目标x-s: XYS_02Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg... x-s-common: Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg... x-t: 1726829333000 x-b3-traceid: 550e8400-e29b-41d4-a716-446655440000 x-xray-traceid: 1234567890abcdef1234567890abcdef x-mns: unload xy-direction: 42有时候还会看到一个x-rap-param: v3_abcdef123456...这些字段里x-t一看就是时间戳毫秒级x-b3-traceid和x-xray-traceid是链路追踪ID格式类似UUID或随机字符串。x-mns和xy-direction看起来像是某种特征或分片标识。而最核心、最复杂的就是x-s和x-s-common它们显然是加密后的签名。3.2 定位签名算法的生成位置如何知道这些头是在哪里生成的呢有两个关键入口搜索关键词在浏览器开发者工具的Sources面板中全局搜索CtrlShiftF字符串片段比如x-s、XYS_、sign、encrypt等。由于代码被混淆直接搜x-s可能找不到但XYS_作为x-s值的固定前缀被硬编码在代码里的可能性很大。XHR/Fetch断点在开发者工具的Sources面板右侧找到XHR/Fetch Breakpoints点击号添加一个包含edith.xiaohongshu.com的URL断点。这样任何向该域名发起的请求在发出前都会暂停此时调用栈Call Stack里就能看到是哪个JavaScript函数最终设置了这些请求头。通过断点我最终定位到了一个被高度混淆的JS文件里面的变量名都是a,b,c,d,e这种。但通过观察调用栈和逐步执行F10可以梳理出大致的调用链条一个公共的请求拦截器可能是基于axios或fetch的封装在请求发出前会调用一个签名生成函数。这个函数接收请求的URL、Method、ParamsGET或PayloadPOST以及当前的Cookies作为输入。3.3 核心参数的提取与猜想在动态调试过程中把签名函数的输入参数和输出的x-s值都记录下来进行多次对比是破解算法的关键。我设计了几个实验实验1固定参数在短时间内用完全相同的URL、参数、Cookie重复发送请求。发现x-t、traceid变了但x-s和x-s-common也变了这说明签名结果不是静态的它至少依赖时间戳或一个随机数。实验2变化路径请求不同的API路径如/user_posted和/feed其他条件尽量相同。x-s值完全不同证明API路径是签名算法的核心输入之一。实验3变化Body对于POST请求稍微改动一下请求体里的一个字段。x-s值也随之改变证明请求体JSON字符串也是核心输入。实验4变化Cookie退出登录或用不同账号请求。x-s和x-s-common都发生巨大变化说明Cookie尤其是a1这个字段是密钥或盐Salt的重要组成部分。基于这些实验我们可以初步假设x-s F(api_path, request_body, cookie_a1, timestamp, ...)其中F是一个不可逆的加密函数很可能是HMAC或AES的变种。而x-s-common看起来像是x-s值的子集或另一种形式的摘要可能用于快速校验。4. x-s与x-s-common算法的深度还原4.1 拆解x-s的构成从密文到明文通过拦截和对比上百条请求我发现x-s的值总是以XYS_开头后面跟着一串很长的、看似随机的字符。这很像一种“标识符Base64编码的密文”格式。我尝试将XYS_后面的部分进行Base64解码。直接解码是乱码说明它可能不是标准的Base64或者解码后是二进制数据。这时一个关键发现是x-s-common的值总是出现在x-s那串长字符的开头部分。例如x-s: XYS_02Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg... x-s-common: Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg...可以看到x-s在XYS_02之后的内容开头就是Bks5b6Hk...与x-s-common完全一致。因此一个合理的推测是x-s-common是核心的签名摘要而x-s是在此基础上添加了前缀和额外的加密或编码层。4.2 逆向核心加密函数在调试器中跟踪到生成x-s-common的函数它最终调用了一个被混淆的、执行加密操作的方法。通过console.log或断点查看其输入和输出我整理出它的逻辑输入拼接将多个参数按特定顺序拼接成一个字符串。顺序通常是HTTP方法GET/POST、请求的URI路径不含域名和查询参数、排序后的查询字符串GET或序列化后的请求体POST、以及一个从Cookie中提取的密钥a1。对于GETinput_str GET/api/sns/web/v1/user_postednum30cursoruser_id123a1_cookie_value对于POSTinput_str POST/api/sns/web/v1/login{username:test,password:123456}a1_cookie_value注意查询参数需要按照字母顺序排序并URL编码请求体需要是紧凑的JSON字符串无多余空格和换行。摘要计算对拼接后的input_str进行哈希计算。逆向发现它使用了HMAC-SHA256算法。密钥Key是一个固定的字符串硬编码在JS中每次版本更新可能会变需要重新抓取。HMAC-SHA256(input_str, fixed_secret_key)得到一个二进制摘要。编码输出将上一步得到的二进制摘要进行Base64编码。但小红书使用的不是标准Base64字符集A-Za-z0-9/而是经过自定义的URL-Safe Base64将/替换为-_并去掉末尾的。这个编码结果就是x-s-common的值。所以x-s-common的生成伪代码如下import hmac import hashlib import base64 def generate_xs_common(method, path, data_str, a1_cookie): # 1. 拼接输入 if method.upper() GET: # data_str 是已排序、URL编码的参数字符串如 num30cursoruser_id123 input_str f{method}{path}{data_str}{a1_cookie} else: # POST # data_str 是紧凑的JSON字符串 input_str f{method}{path}{data_str}{a1_cookie} # 2. HMAC-SHA256 计算密钥是逆向出的固定值 fixed_secret_key b逆向出的固定密钥字节串 digest hmac.new(fixed_secret_key, input_str.encode(utf-8), hashlib.sha256).digest() # 3. URL-Safe Base64 编码 xs_common base64.urlsafe_b64encode(digest).decode(utf-8).rstrip() return xs_common4.3 解析x-s的完整生成流程得到了x-s-commonx-s的生成就相对清晰了。继续跟踪代码发现x-s是x-s-common经过进一步处理得到的添加版本标识在x-s-common前面加上一个两位的版本号例如02。这个版本号可能对应不同的算法迭代。二次加密/编码将版本号 x-s-common这个字符串再进行一次加密变换。逆向发现这里使用了一个简单的**异或XOR**操作与一个固定的字节数组可以理解为另一个密钥进行逐字节异或。最终编码与添加前缀将异或后的结果再次进行URL-Safe Base64编码然后在最前面加上固定的前缀XYS_最终形成x-s头。所以x-s的生成伪代码如下def generate_xs(xs_common, version02): # 1. 拼接版本号 raw_str version xs_common # 2. 与固定密钥进行XOR需要将字符串转为字节操作 xor_key b逆向出的XOR密钥字节数组 raw_bytes raw_str.encode(utf-8) # 注意xor_key可能需要循环使用如果raw_bytes更长 xored_bytes bytes([raw_bytes[i] ^ xor_key[i % len(xor_key)] for i in range(len(raw_bytes))]) # 3. 编码并加前缀 xs_encoded base64.urlsafe_b64encode(xored_bytes).decode(utf-8).rstrip() final_xs fXYS_{xs_encoded} return final_xs实操心得这里的XOR密钥和HMAC的固定密钥是算法还原中最关键、也最隐蔽的部分。它们被深度混淆在JS代码中可能被编码成数组、字符串或者通过一系列位运算动态生成。我的方法是在调试器中在计算HMAC和XOR的代码行设置断点直接查看此时传入的key变量的值。如果它是从某个复杂函数计算出来的就继续向上追溯直到找到最原始的常量。这个过程需要耐心有时密钥会被拆分成多个片段在运行时拼接。5. 其他辅助字段的生成与作用5.1 时间戳与追踪IDx-t, x-b3-traceid, x-xray-traceid这些字段的生成相对简单但必须保持逻辑一致。x-t13位毫秒级Unix时间戳int(time.time() * 1000)。重要同一个请求中所有需要时间戳的字段包括签名算法内部可能用到的时间戳最好使用同一个时间戳值以避免微小的系统时间差导致签名校验失败。x-b3-traceid一个16字节32位十六进制字符的随机字符串通常符合分布式追踪标准如Zipkin。可以用UUID或者随机生成。x-xray-traceid一个32字节64位十六进制字符的随机字符串。生成方式类似。在Python中可以这样生成import time import uuid import secrets timestamp int(time.time() * 1000) # 统一的时间戳 x_t str(timestamp) x_b3_traceid uuid.uuid4().hex # 32位hex x_xray_traceid secrets.token_hex(16) # 32字节 - 64位hex5.2 分片标识xy-direction这个字段的值看起来像一个小整数如42。通过分析发现它与当前登录用户的user_id有关。算法是对user_id字符串进行MurmurHash3哈希计算然后将结果对一个固定的数比如100取模得到最终的分片值。如果请求不涉及特定用户如未登录浏览则可能随机生成一个。MurmurHash3是一种非加密哈希函数速度快、碰撞率低。在Python中可以使用mmh3库。import mmh3 def get_xy_direction(user_idNone): if user_id: hash_int mmh3.hash(user_id) # 默认种子为0 return str(abs(hash_int) % 100) # 取模得到0-99的数 else: return str(random.randint(0, 99))5.3 动态风控参数x-rap-param这个头并非所有接口都需要但在调用信息流feed、搜索、发布笔记等核心敏感接口时是必须的。它的生成算法更为独立但思路类似。x-rap-param的值通常以v3_开头后面跟着一串编码字符串。逆向发现它的生成依赖于API路径同样是请求的URI路径。请求体POST请求的JSON body。一个独立的密钥与生成x-s的密钥不同是另一套硬编码的常量。其算法也是拼接路径和body然后用特定的密钥进行HMAC-SHA256哈希最后进行自定义的Base62或Base64编码字符集可能不同并加上v3_前缀。注意事项x-rap-param的密钥和编码方式可能比x-s更新得更频繁因为它直接关联到风控系统。如果发现带有x-rap-param的接口突然全部失效而普通接口正常首先要怀疑的就是这个算法已经更新。6. 完整算法实现与Python代码封装将上述所有步骤整合我们就得到了一个完整的小红书签名生成库。下面给出一个高度精简但功能完整的Python类示例展示了核心逻辑。在实际使用中你需要将FIXED_SECRET_KEY和XOR_KEY替换为逆向出的真实值。import hmac import hashlib import base64 import time import uuid import random from urllib.parse import urlencode, quote_plus import json class XHS_Signer: def __init__(self): # !!! 警告以下密钥为示例需替换为真实逆向出的值 !!! self.FIXED_SECRET_KEY byour_real_hmac_key_here # HMAC-SHA256的密钥 self.XOR_KEY byour_real_xor_key_here # 二次XOR加密的密钥 self.XRAP_SECRET_KEY byour_real_xrap_key_here # x-rap-param的密钥 def _urlsafe_b64encode(self, data): URL安全的Base64编码去掉末尾的号 return base64.urlsafe_b64encode(data).decode(utf-8).rstrip() def _generate_xs_common(self, method, path, data_str, a1_cookie): 生成x-s-common # 拼接输入字符串 input_str f{method.upper()}{path}{data_str}{a1_cookie} # 计算HMAC-SHA256 digest hmac.new(self.FIXED_SECRET_KEY, input_str.encode(utf-8), hashlib.sha256).digest() # URL-Safe Base64编码 return self._urlsafe_b64encode(digest) def _generate_xs(self, xs_common, version02): 基于x-s-common生成x-s raw_str version xs_common raw_bytes raw_str.encode(utf-8) # 与XOR_KEY进行循环异或 xored_bytes bytes([raw_bytes[i] ^ self.XOR_KEY[i % len(self.XOR_KEY)] for i in range(len(raw_bytes))]) encoded self._urlsafe_b64encode(xored_bytes) return fXYS_{encoded} def generate_headers(self, method, url, paramsNone, payloadNone, cookiesNone, need_xrapFalse, user_idNone): 生成完整的签名请求头 :param method: GET/POST :param url: 完整的请求URL :param params: GET请求的参数字典 :param payload: POST请求的JSON体字典 :param cookies: 字典形式的cookies必须包含a1 :param need_xrap: 是否需要生成x-rap-param :param user_id: 用户ID用于计算xy-direction :return: 包含所有签名头的字典 from urllib.parse import urlparse # 1. 解析URL获取路径 parsed_url urlparse(url) path parsed_url.path # 2. 准备数据字符串 data_str if method.upper() GET and params: # GET参数需要排序并URL编码 sorted_params sorted(params.items(), keylambda x: x[0]) data_str urlencode(sorted_params, quote_viaquote_plus) elif method.upper() POST and payload: # POST body需要紧凑的JSON data_str json.dumps(payload, separators(,, :)) # 3. 获取a1 cookie a1_cookie cookies.get(a1, ) if isinstance(cookies, dict) else if not a1_cookie and isinstance(cookies, str): # 如果cookies是字符串尝试解析 # 简单实现实际应用需更健壮的解析 pass # 4. 生成x-s-common和x-s xs_common self._generate_xs_common(method, path, data_str, a1_cookie) xs self._generate_xs(xs_common) # 5. 生成其他固定头 timestamp int(time.time() * 1000) headers { x-s: xs, x-s-common: xs_common, x-t: str(timestamp), x-b3-traceid: uuid.uuid4().hex, x-xray-traceid: uuid.uuid4().hex * 2, # 64位hex x-mns: unload, # 通常固定为此值 } # 6. 生成xy-direction if user_id: import mmh3 direction abs(mmh3.hash(user_id)) % 100 else: direction random.randint(0, 99) headers[xy-direction] str(direction) # 7. 生成x-rap-param (如果需要) if need_xrap: rap_input f{path}{data_str} if method.upper() POST else path rap_digest hmac.new(self.XRAP_SECRET_KEY, rap_input.encode(utf-8), hashlib.sha256).digest() rap_encoded self._urlsafe_b64encode(rap_digest) headers[x-rap-param] fv3_{rap_encoded} return headers # 使用示例 if __name__ __main__: signer XHS_Signer() # 注意需要替换为真实的密钥和cookie cookies {a1: your_real_a1_cookie_here} # 模拟GET请求 get_headers signer.generate_headers( methodGET, urlhttps://edith.xiaohongshu.com/api/sns/web/v1/user_posted, params{num: 30, cursor: , user_id: 123}, cookiescookies, user_id123 ) print(GET Headers:, json.dumps(get_headers, indent2)) # 模拟POST请求需要x-rap-param post_headers signer.generate_headers( methodPOST, urlhttps://edith.xiaohongshu.com/api/sns/web/v1/feed, payload{source_note_id: abcdefg}, cookiescookies, need_xrapTrue, user_id123 ) print(\nPOST Headers:, json.dumps(post_headers, indent2))这个类封装了主要的签名逻辑。在实际项目中你可以将其打包成库并添加会话管理、错误重试、密钥自动更新等高级功能。7. 常见问题排查与实战避坑指南即使算法还原正确在实际调用中也可能遇到各种问题。下面是我在实战中踩过的坑和解决方案。7.1 签名校验失败403错误这是最常见的问题。请按以下清单逐一核对问题可能点检查项与解决方案密钥错误这是最可能的原因。确认FIXED_SECRET_KEY、XOR_KEY、XRAP_SECRET_KEY是否与当前小红书版本匹配。密钥可能已更新需要重新抓包逆向。输入字符串格式1.方法名必须是全大写的GET或POST。2.路径必须是URI路径如/api/sns/web/v1/user_posted不能包含域名、协议或查询字符串。3.参数字符串GET参数需按字母顺序排序并正确进行URL编码空格转为或%20。4.请求体必须是紧凑的、无空白字符的JSON字符串。json.dumps(payload, separators(,, :))是关键。Cookie缺失或错误a1cookie是签名的关键输入。确保传入的a1值正确且未过期。可以通过浏览器开发者工具的Application-Cookies面板查看。时间戳不同步确保x-t头的时间戳与生成签名时内部可能用到的时间戳如果有是同一时刻的值。最好在函数开始时获取一个时间戳所有地方都使用它。x-rap-param缺失调用feed、search等接口时必须设置need_xrapTrue。7.2 请求被限流或封禁即使签名正确高频或异常请求也会触发风控。控制请求频率在请求间添加随机延迟如1-3秒模拟人类操作。避免使用固定间隔。维护合理的Cookie池一个账号短时间内请求过多必然被限。需要准备多个账号的Cookie进行轮换。模拟完整会话除了签名头其他常规头如User-Agent、Referer、Accept-Language也要设置得合理最好从真实浏览器请求中复制。注意xy-direction这个分片标识如果一直固定或随机可能不如用真实user_id计算来得“真实”。尽量传入有效的user_id。7.3 算法更新与维护小红书的反爬策略是动态升级的。如何及时发现算法失效建立监控在你的自动化脚本中对接口返回的HTTP状态码和响应体进行监控。如果连续出现大量403或响应体包含“签名错误”、“风控”等关键字很可能算法已更新。定期采样验证每天用你的算法生成签名与通过浏览器正常访问抓取到的签名进行对比。如果发现对同一请求的签名结果不同立即启动重新分析。关注社区像xhshow这样的开源项目其Issue页面或社区讨论往往是算法更新的第一线情报站。7.4 关于xhshow开源库的使用与局限在分析过程中我参考了GitHub上的开源项目xhshow。它是一个优秀的Python实现封装得很好。它的优势在于开箱即用并且处理了很多边界情况如URL构建、会话管理。但直接使用它也有需要注意的地方密钥内置库中硬编码的密钥可能不是最新的。你需要确认其版本是否支持当前时间点的小红书接口。依赖更新如果小红书更新算法你需要等待库作者更新或者自己Fork项目更新密钥。理解原理直接调库虽然方便但遇到问题时很难排查。通过本文的逆向过程理解原理后你就能更从容地使用或修改这类库。我的建议是将xhshow这样的库作为生产环境的实现参考和备用方案但自己必须掌握核心算法的还原和更新能力。这样在库失效时你能快速自己动手修复。8. 进阶思考签名算法的对抗与演化逆向工程师和平台安全工程师是一场持续的博弈。小红书以及其他大型平台的签名算法未来可能会朝哪些方向演化我们又该如何应对动态密钥与代码混淆加强密钥不再硬编码而是由服务器动态下发或者通过更复杂的JS虚拟机如WebAssembly来执行加密逻辑增加静态分析的难度。应对策略加强动态调试能力关注网络请求中是否包含加密相关的JS代码块或密钥数据。环境指纹绑定签名算法可能不仅依赖请求参数还会融入浏览器指纹Canvas, WebGL, AudioContext等、设备特征、甚至用户行为序列。生成一个与环境绑定的Token签名时需要用到它。应对策略需要更完整的浏览器环境模拟或者研究如何提取和复用合法的环境指纹。算法分片与延迟执行加密代码被拆分成无数个碎片分布在不同的JS文件或网络请求中在运行时动态拼接和执行。应对策略需要更全局的请求监控和JS执行跟踪还原完整的代码执行流。走向纯服务端验证最彻底的方式将核心验证逻辑完全放在服务端前端只负责传递参数由服务端返回一个短期有效的令牌Token。这会让纯前端算法还原变得几乎不可能。应对策略这种方案下可能需要考虑其他技术路径如自动化测试框架的合法使用或寻找官方开放的API。逆向工程没有一劳永逸的解决方案。它考验的是持续学习、分析问题和动手实现的能力。这次对小红书x-s算法的还原是一次典型的前端加密逆向实战其中涉及的抓包、调试、逻辑分析、算法实现等技能在分析其他平台时也是通用的。保持好奇心耐心跟踪每一个细节你就能解开大部分看似复杂的签名黑盒。
小红书x-s签名算法逆向实战:HMAC-SHA256与Base64编码的接口防护破解
发布时间:2026/7/4 10:54:12
1. 项目概述小红书x-s签名算法的逆向工程实战最近在搞小红书数据相关的项目发现它的接口防护又升级了特别是那个x-s签名简直是爬虫和自动化工具的头号拦路虎。我花了差不多一周时间从抓包分析到算法还原总算把2024年9月这个最新版本的x-s、x-s-common、x-rap-param这几个核心请求头的生成逻辑给摸透了。这玩意儿本质上是一个基于请求路径和请求体Body的加密签名算法无论是GET还是POST请求都得用它目的就是为了防止非法的接口调用和数据爬取。如果你也遇到过调用小红书API时返回403 Forbidden或者签名错误那这篇文章就是为你准备的。我会从最基础的抓包开始带你一步步拆解整个签名算法的构成包括如何定位关键加密函数、如何还原核心的加密逻辑最后还会分享一个可以直接拿来用的Python实现库。整个过程不涉及任何客户端模拟或者RPC调用是纯粹的算法还原Pure Calculation这意味着你可以在服务器端、任何编程语言中复现这个签名稳定性极高。2. 逆向工程的核心思路与准备工作2.1 为什么选择“纯算”而非RPC或模拟在逆向一个App的接口时通常有几条路可以走一是使用frida、xposed等框架进行RPCRemote Procedure Call调用直接调用App内部的加密函数二是使用playwright、appium等工具进行完整的UI或协议模拟三就是我们这次要做的“算法还原”。RPC调用看起来最省事直接从内存里捞结果。但它有几个致命缺点一是严重依赖特定的App版本小红书一更新你就得重新找偏移地址维护成本高二是需要在移动设备环境或模拟器中运行难以集成到服务端的自动化流程里三是存在法律和安全风险。模拟的方式则太重了资源消耗大效率低。而算法还原就是通过静态分析看代码和动态调试看执行过程把黑盒的加密过程变成白盒的可计算公式。一旦还原成功你就可以用任何语言、在任何地方生成完全合法的签名。它的好处是稳定、高效、可移植缺点是前期分析过程比较烧脑。但对于小红书这种核心业务接口算法在短期内不会频繁变动投入一次是值得的。2.2 必要的工具与环境搭建工欲善其事必先利其器。逆向分析需要一套组合工具抓包工具这是所有分析的起点。推荐使用Charles或Fiddler Everywhere它们对HTTPS流量的解密和重放支持得很好。关键是要在你的测试手机和电脑上安装好CA证书并开启SSL代理。反编译与调试工具对于Android应用Jadx-GUI是反编译APK查看Java代码的神器。对于Web端或小程序浏览器开发者工具F12中的Sources面板和Debugger就是主战场。如果遇到Vue或React打包的代码可能需要sourcemap或者借助AST解析工具来还原。JavaScript调试环境因为签名算法大概率在前端JavaScript中执行。一个无头浏览器环境如Puppeteer或Playwright可以用来动态加载页面并注入调试代码。更直接的方式是使用Node.js配合vm2模块来安全地执行和调试提取出来的JS代码片段。编程语言分析过程中用Python写辅助脚本非常方便比如自动化的参数对比、算法验证等。最终算法还原后你也可以用Python、Golang、Java等任何语言来实现。我的分析环境是macOS系统使用Charles抓包针对小红书Web端edith.xiaohongshu.com进行分析。选择Web端是因为其JavaScript代码虽然经过混淆但相对于移动端Native代码动态调试的门槛还是低一些。注意所有分析请务必基于你自己有权限访问的账号和数据严格遵守robots.txt协议仅用于学习和技术交流目的避免高频请求对目标服务器造成压力。3. 签名算法关键字段的抓取与分析3.1 初识小红书接口的签名Headers首先打开Charles设置好代理然后在浏览器中正常访问小红书官网并登录。随意点击几个笔记刷新一下主页你会在Charles中看到大量向edith.xiaohongshu.com这个域名发起的请求。我们找一个典型的接口看看比如获取用户发布列表的GET https://edith.xiaohongshu.com/api/sns/web/v1/user_posted?num30cursoruser_idxxx。查看这个请求的Request Headers你会发现一堆以x-开头的自定义头它们就是我们的目标x-s: XYS_02Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg... x-s-common: Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg... x-t: 1726829333000 x-b3-traceid: 550e8400-e29b-41d4-a716-446655440000 x-xray-traceid: 1234567890abcdef1234567890abcdef x-mns: unload xy-direction: 42有时候还会看到一个x-rap-param: v3_abcdef123456...这些字段里x-t一看就是时间戳毫秒级x-b3-traceid和x-xray-traceid是链路追踪ID格式类似UUID或随机字符串。x-mns和xy-direction看起来像是某种特征或分片标识。而最核心、最复杂的就是x-s和x-s-common它们显然是加密后的签名。3.2 定位签名算法的生成位置如何知道这些头是在哪里生成的呢有两个关键入口搜索关键词在浏览器开发者工具的Sources面板中全局搜索CtrlShiftF字符串片段比如x-s、XYS_、sign、encrypt等。由于代码被混淆直接搜x-s可能找不到但XYS_作为x-s值的固定前缀被硬编码在代码里的可能性很大。XHR/Fetch断点在开发者工具的Sources面板右侧找到XHR/Fetch Breakpoints点击号添加一个包含edith.xiaohongshu.com的URL断点。这样任何向该域名发起的请求在发出前都会暂停此时调用栈Call Stack里就能看到是哪个JavaScript函数最终设置了这些请求头。通过断点我最终定位到了一个被高度混淆的JS文件里面的变量名都是a,b,c,d,e这种。但通过观察调用栈和逐步执行F10可以梳理出大致的调用链条一个公共的请求拦截器可能是基于axios或fetch的封装在请求发出前会调用一个签名生成函数。这个函数接收请求的URL、Method、ParamsGET或PayloadPOST以及当前的Cookies作为输入。3.3 核心参数的提取与猜想在动态调试过程中把签名函数的输入参数和输出的x-s值都记录下来进行多次对比是破解算法的关键。我设计了几个实验实验1固定参数在短时间内用完全相同的URL、参数、Cookie重复发送请求。发现x-t、traceid变了但x-s和x-s-common也变了这说明签名结果不是静态的它至少依赖时间戳或一个随机数。实验2变化路径请求不同的API路径如/user_posted和/feed其他条件尽量相同。x-s值完全不同证明API路径是签名算法的核心输入之一。实验3变化Body对于POST请求稍微改动一下请求体里的一个字段。x-s值也随之改变证明请求体JSON字符串也是核心输入。实验4变化Cookie退出登录或用不同账号请求。x-s和x-s-common都发生巨大变化说明Cookie尤其是a1这个字段是密钥或盐Salt的重要组成部分。基于这些实验我们可以初步假设x-s F(api_path, request_body, cookie_a1, timestamp, ...)其中F是一个不可逆的加密函数很可能是HMAC或AES的变种。而x-s-common看起来像是x-s值的子集或另一种形式的摘要可能用于快速校验。4. x-s与x-s-common算法的深度还原4.1 拆解x-s的构成从密文到明文通过拦截和对比上百条请求我发现x-s的值总是以XYS_开头后面跟着一串很长的、看似随机的字符。这很像一种“标识符Base64编码的密文”格式。我尝试将XYS_后面的部分进行Base64解码。直接解码是乱码说明它可能不是标准的Base64或者解码后是二进制数据。这时一个关键发现是x-s-common的值总是出现在x-s那串长字符的开头部分。例如x-s: XYS_02Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg... x-s-common: Bks5b6Hk8wHkP1A7z4SgXxXpN8MFaBZg...可以看到x-s在XYS_02之后的内容开头就是Bks5b6Hk...与x-s-common完全一致。因此一个合理的推测是x-s-common是核心的签名摘要而x-s是在此基础上添加了前缀和额外的加密或编码层。4.2 逆向核心加密函数在调试器中跟踪到生成x-s-common的函数它最终调用了一个被混淆的、执行加密操作的方法。通过console.log或断点查看其输入和输出我整理出它的逻辑输入拼接将多个参数按特定顺序拼接成一个字符串。顺序通常是HTTP方法GET/POST、请求的URI路径不含域名和查询参数、排序后的查询字符串GET或序列化后的请求体POST、以及一个从Cookie中提取的密钥a1。对于GETinput_str GET/api/sns/web/v1/user_postednum30cursoruser_id123a1_cookie_value对于POSTinput_str POST/api/sns/web/v1/login{username:test,password:123456}a1_cookie_value注意查询参数需要按照字母顺序排序并URL编码请求体需要是紧凑的JSON字符串无多余空格和换行。摘要计算对拼接后的input_str进行哈希计算。逆向发现它使用了HMAC-SHA256算法。密钥Key是一个固定的字符串硬编码在JS中每次版本更新可能会变需要重新抓取。HMAC-SHA256(input_str, fixed_secret_key)得到一个二进制摘要。编码输出将上一步得到的二进制摘要进行Base64编码。但小红书使用的不是标准Base64字符集A-Za-z0-9/而是经过自定义的URL-Safe Base64将/替换为-_并去掉末尾的。这个编码结果就是x-s-common的值。所以x-s-common的生成伪代码如下import hmac import hashlib import base64 def generate_xs_common(method, path, data_str, a1_cookie): # 1. 拼接输入 if method.upper() GET: # data_str 是已排序、URL编码的参数字符串如 num30cursoruser_id123 input_str f{method}{path}{data_str}{a1_cookie} else: # POST # data_str 是紧凑的JSON字符串 input_str f{method}{path}{data_str}{a1_cookie} # 2. HMAC-SHA256 计算密钥是逆向出的固定值 fixed_secret_key b逆向出的固定密钥字节串 digest hmac.new(fixed_secret_key, input_str.encode(utf-8), hashlib.sha256).digest() # 3. URL-Safe Base64 编码 xs_common base64.urlsafe_b64encode(digest).decode(utf-8).rstrip() return xs_common4.3 解析x-s的完整生成流程得到了x-s-commonx-s的生成就相对清晰了。继续跟踪代码发现x-s是x-s-common经过进一步处理得到的添加版本标识在x-s-common前面加上一个两位的版本号例如02。这个版本号可能对应不同的算法迭代。二次加密/编码将版本号 x-s-common这个字符串再进行一次加密变换。逆向发现这里使用了一个简单的**异或XOR**操作与一个固定的字节数组可以理解为另一个密钥进行逐字节异或。最终编码与添加前缀将异或后的结果再次进行URL-Safe Base64编码然后在最前面加上固定的前缀XYS_最终形成x-s头。所以x-s的生成伪代码如下def generate_xs(xs_common, version02): # 1. 拼接版本号 raw_str version xs_common # 2. 与固定密钥进行XOR需要将字符串转为字节操作 xor_key b逆向出的XOR密钥字节数组 raw_bytes raw_str.encode(utf-8) # 注意xor_key可能需要循环使用如果raw_bytes更长 xored_bytes bytes([raw_bytes[i] ^ xor_key[i % len(xor_key)] for i in range(len(raw_bytes))]) # 3. 编码并加前缀 xs_encoded base64.urlsafe_b64encode(xored_bytes).decode(utf-8).rstrip() final_xs fXYS_{xs_encoded} return final_xs实操心得这里的XOR密钥和HMAC的固定密钥是算法还原中最关键、也最隐蔽的部分。它们被深度混淆在JS代码中可能被编码成数组、字符串或者通过一系列位运算动态生成。我的方法是在调试器中在计算HMAC和XOR的代码行设置断点直接查看此时传入的key变量的值。如果它是从某个复杂函数计算出来的就继续向上追溯直到找到最原始的常量。这个过程需要耐心有时密钥会被拆分成多个片段在运行时拼接。5. 其他辅助字段的生成与作用5.1 时间戳与追踪IDx-t, x-b3-traceid, x-xray-traceid这些字段的生成相对简单但必须保持逻辑一致。x-t13位毫秒级Unix时间戳int(time.time() * 1000)。重要同一个请求中所有需要时间戳的字段包括签名算法内部可能用到的时间戳最好使用同一个时间戳值以避免微小的系统时间差导致签名校验失败。x-b3-traceid一个16字节32位十六进制字符的随机字符串通常符合分布式追踪标准如Zipkin。可以用UUID或者随机生成。x-xray-traceid一个32字节64位十六进制字符的随机字符串。生成方式类似。在Python中可以这样生成import time import uuid import secrets timestamp int(time.time() * 1000) # 统一的时间戳 x_t str(timestamp) x_b3_traceid uuid.uuid4().hex # 32位hex x_xray_traceid secrets.token_hex(16) # 32字节 - 64位hex5.2 分片标识xy-direction这个字段的值看起来像一个小整数如42。通过分析发现它与当前登录用户的user_id有关。算法是对user_id字符串进行MurmurHash3哈希计算然后将结果对一个固定的数比如100取模得到最终的分片值。如果请求不涉及特定用户如未登录浏览则可能随机生成一个。MurmurHash3是一种非加密哈希函数速度快、碰撞率低。在Python中可以使用mmh3库。import mmh3 def get_xy_direction(user_idNone): if user_id: hash_int mmh3.hash(user_id) # 默认种子为0 return str(abs(hash_int) % 100) # 取模得到0-99的数 else: return str(random.randint(0, 99))5.3 动态风控参数x-rap-param这个头并非所有接口都需要但在调用信息流feed、搜索、发布笔记等核心敏感接口时是必须的。它的生成算法更为独立但思路类似。x-rap-param的值通常以v3_开头后面跟着一串编码字符串。逆向发现它的生成依赖于API路径同样是请求的URI路径。请求体POST请求的JSON body。一个独立的密钥与生成x-s的密钥不同是另一套硬编码的常量。其算法也是拼接路径和body然后用特定的密钥进行HMAC-SHA256哈希最后进行自定义的Base62或Base64编码字符集可能不同并加上v3_前缀。注意事项x-rap-param的密钥和编码方式可能比x-s更新得更频繁因为它直接关联到风控系统。如果发现带有x-rap-param的接口突然全部失效而普通接口正常首先要怀疑的就是这个算法已经更新。6. 完整算法实现与Python代码封装将上述所有步骤整合我们就得到了一个完整的小红书签名生成库。下面给出一个高度精简但功能完整的Python类示例展示了核心逻辑。在实际使用中你需要将FIXED_SECRET_KEY和XOR_KEY替换为逆向出的真实值。import hmac import hashlib import base64 import time import uuid import random from urllib.parse import urlencode, quote_plus import json class XHS_Signer: def __init__(self): # !!! 警告以下密钥为示例需替换为真实逆向出的值 !!! self.FIXED_SECRET_KEY byour_real_hmac_key_here # HMAC-SHA256的密钥 self.XOR_KEY byour_real_xor_key_here # 二次XOR加密的密钥 self.XRAP_SECRET_KEY byour_real_xrap_key_here # x-rap-param的密钥 def _urlsafe_b64encode(self, data): URL安全的Base64编码去掉末尾的号 return base64.urlsafe_b64encode(data).decode(utf-8).rstrip() def _generate_xs_common(self, method, path, data_str, a1_cookie): 生成x-s-common # 拼接输入字符串 input_str f{method.upper()}{path}{data_str}{a1_cookie} # 计算HMAC-SHA256 digest hmac.new(self.FIXED_SECRET_KEY, input_str.encode(utf-8), hashlib.sha256).digest() # URL-Safe Base64编码 return self._urlsafe_b64encode(digest) def _generate_xs(self, xs_common, version02): 基于x-s-common生成x-s raw_str version xs_common raw_bytes raw_str.encode(utf-8) # 与XOR_KEY进行循环异或 xored_bytes bytes([raw_bytes[i] ^ self.XOR_KEY[i % len(self.XOR_KEY)] for i in range(len(raw_bytes))]) encoded self._urlsafe_b64encode(xored_bytes) return fXYS_{encoded} def generate_headers(self, method, url, paramsNone, payloadNone, cookiesNone, need_xrapFalse, user_idNone): 生成完整的签名请求头 :param method: GET/POST :param url: 完整的请求URL :param params: GET请求的参数字典 :param payload: POST请求的JSON体字典 :param cookies: 字典形式的cookies必须包含a1 :param need_xrap: 是否需要生成x-rap-param :param user_id: 用户ID用于计算xy-direction :return: 包含所有签名头的字典 from urllib.parse import urlparse # 1. 解析URL获取路径 parsed_url urlparse(url) path parsed_url.path # 2. 准备数据字符串 data_str if method.upper() GET and params: # GET参数需要排序并URL编码 sorted_params sorted(params.items(), keylambda x: x[0]) data_str urlencode(sorted_params, quote_viaquote_plus) elif method.upper() POST and payload: # POST body需要紧凑的JSON data_str json.dumps(payload, separators(,, :)) # 3. 获取a1 cookie a1_cookie cookies.get(a1, ) if isinstance(cookies, dict) else if not a1_cookie and isinstance(cookies, str): # 如果cookies是字符串尝试解析 # 简单实现实际应用需更健壮的解析 pass # 4. 生成x-s-common和x-s xs_common self._generate_xs_common(method, path, data_str, a1_cookie) xs self._generate_xs(xs_common) # 5. 生成其他固定头 timestamp int(time.time() * 1000) headers { x-s: xs, x-s-common: xs_common, x-t: str(timestamp), x-b3-traceid: uuid.uuid4().hex, x-xray-traceid: uuid.uuid4().hex * 2, # 64位hex x-mns: unload, # 通常固定为此值 } # 6. 生成xy-direction if user_id: import mmh3 direction abs(mmh3.hash(user_id)) % 100 else: direction random.randint(0, 99) headers[xy-direction] str(direction) # 7. 生成x-rap-param (如果需要) if need_xrap: rap_input f{path}{data_str} if method.upper() POST else path rap_digest hmac.new(self.XRAP_SECRET_KEY, rap_input.encode(utf-8), hashlib.sha256).digest() rap_encoded self._urlsafe_b64encode(rap_digest) headers[x-rap-param] fv3_{rap_encoded} return headers # 使用示例 if __name__ __main__: signer XHS_Signer() # 注意需要替换为真实的密钥和cookie cookies {a1: your_real_a1_cookie_here} # 模拟GET请求 get_headers signer.generate_headers( methodGET, urlhttps://edith.xiaohongshu.com/api/sns/web/v1/user_posted, params{num: 30, cursor: , user_id: 123}, cookiescookies, user_id123 ) print(GET Headers:, json.dumps(get_headers, indent2)) # 模拟POST请求需要x-rap-param post_headers signer.generate_headers( methodPOST, urlhttps://edith.xiaohongshu.com/api/sns/web/v1/feed, payload{source_note_id: abcdefg}, cookiescookies, need_xrapTrue, user_id123 ) print(\nPOST Headers:, json.dumps(post_headers, indent2))这个类封装了主要的签名逻辑。在实际项目中你可以将其打包成库并添加会话管理、错误重试、密钥自动更新等高级功能。7. 常见问题排查与实战避坑指南即使算法还原正确在实际调用中也可能遇到各种问题。下面是我在实战中踩过的坑和解决方案。7.1 签名校验失败403错误这是最常见的问题。请按以下清单逐一核对问题可能点检查项与解决方案密钥错误这是最可能的原因。确认FIXED_SECRET_KEY、XOR_KEY、XRAP_SECRET_KEY是否与当前小红书版本匹配。密钥可能已更新需要重新抓包逆向。输入字符串格式1.方法名必须是全大写的GET或POST。2.路径必须是URI路径如/api/sns/web/v1/user_posted不能包含域名、协议或查询字符串。3.参数字符串GET参数需按字母顺序排序并正确进行URL编码空格转为或%20。4.请求体必须是紧凑的、无空白字符的JSON字符串。json.dumps(payload, separators(,, :))是关键。Cookie缺失或错误a1cookie是签名的关键输入。确保传入的a1值正确且未过期。可以通过浏览器开发者工具的Application-Cookies面板查看。时间戳不同步确保x-t头的时间戳与生成签名时内部可能用到的时间戳如果有是同一时刻的值。最好在函数开始时获取一个时间戳所有地方都使用它。x-rap-param缺失调用feed、search等接口时必须设置need_xrapTrue。7.2 请求被限流或封禁即使签名正确高频或异常请求也会触发风控。控制请求频率在请求间添加随机延迟如1-3秒模拟人类操作。避免使用固定间隔。维护合理的Cookie池一个账号短时间内请求过多必然被限。需要准备多个账号的Cookie进行轮换。模拟完整会话除了签名头其他常规头如User-Agent、Referer、Accept-Language也要设置得合理最好从真实浏览器请求中复制。注意xy-direction这个分片标识如果一直固定或随机可能不如用真实user_id计算来得“真实”。尽量传入有效的user_id。7.3 算法更新与维护小红书的反爬策略是动态升级的。如何及时发现算法失效建立监控在你的自动化脚本中对接口返回的HTTP状态码和响应体进行监控。如果连续出现大量403或响应体包含“签名错误”、“风控”等关键字很可能算法已更新。定期采样验证每天用你的算法生成签名与通过浏览器正常访问抓取到的签名进行对比。如果发现对同一请求的签名结果不同立即启动重新分析。关注社区像xhshow这样的开源项目其Issue页面或社区讨论往往是算法更新的第一线情报站。7.4 关于xhshow开源库的使用与局限在分析过程中我参考了GitHub上的开源项目xhshow。它是一个优秀的Python实现封装得很好。它的优势在于开箱即用并且处理了很多边界情况如URL构建、会话管理。但直接使用它也有需要注意的地方密钥内置库中硬编码的密钥可能不是最新的。你需要确认其版本是否支持当前时间点的小红书接口。依赖更新如果小红书更新算法你需要等待库作者更新或者自己Fork项目更新密钥。理解原理直接调库虽然方便但遇到问题时很难排查。通过本文的逆向过程理解原理后你就能更从容地使用或修改这类库。我的建议是将xhshow这样的库作为生产环境的实现参考和备用方案但自己必须掌握核心算法的还原和更新能力。这样在库失效时你能快速自己动手修复。8. 进阶思考签名算法的对抗与演化逆向工程师和平台安全工程师是一场持续的博弈。小红书以及其他大型平台的签名算法未来可能会朝哪些方向演化我们又该如何应对动态密钥与代码混淆加强密钥不再硬编码而是由服务器动态下发或者通过更复杂的JS虚拟机如WebAssembly来执行加密逻辑增加静态分析的难度。应对策略加强动态调试能力关注网络请求中是否包含加密相关的JS代码块或密钥数据。环境指纹绑定签名算法可能不仅依赖请求参数还会融入浏览器指纹Canvas, WebGL, AudioContext等、设备特征、甚至用户行为序列。生成一个与环境绑定的Token签名时需要用到它。应对策略需要更完整的浏览器环境模拟或者研究如何提取和复用合法的环境指纹。算法分片与延迟执行加密代码被拆分成无数个碎片分布在不同的JS文件或网络请求中在运行时动态拼接和执行。应对策略需要更全局的请求监控和JS执行跟踪还原完整的代码执行流。走向纯服务端验证最彻底的方式将核心验证逻辑完全放在服务端前端只负责传递参数由服务端返回一个短期有效的令牌Token。这会让纯前端算法还原变得几乎不可能。应对策略这种方案下可能需要考虑其他技术路径如自动化测试框架的合法使用或寻找官方开放的API。逆向工程没有一劳永逸的解决方案。它考验的是持续学习、分析问题和动手实现的能力。这次对小红书x-s算法的还原是一次典型的前端加密逆向实战其中涉及的抓包、调试、逻辑分析、算法实现等技能在分析其他平台时也是通用的。保持好奇心耐心跟踪每一个细节你就能解开大部分看似复杂的签名黑盒。