ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TikTok前端动态签名逆向:X-bougs与_signature协议解析

TikTok前端动态签名逆向:X-bougs与_signature协议解析 1. 这不是“爬虫技巧”而是一次标准的前端协议逆向工程实战你打开TikTok网页版想抓取几条视频数据做内容分析刚写好请求curl一发——返回{status_code:10000,status_msg:invalid signature detected}。别急着换UA、加Referer、重装浏览器这个报错背后藏着一套完整的、被精心设计的前端安全协议。它既不是简单的MD5或时间戳拼接也不是靠暴力穷举能破解的哈希而是TikTok在2023年中后期全面启用的一套动态签名机制核心就落在两个字段上X-bougs和_signature。这两个参数一个藏在请求头里X-bougs一个塞在URL查询参数中_signature它们共同构成了一道“双因子门禁”。缺一不可顺序不能乱过期时间精确到秒级。我第一次遇到它时也以为是常规的加密混淆直接丢进AST还原工具跑了一整晚结果生成的JS代码根本跑不通——因为它的签名逻辑根本不在静态JS里而是在Chrome DevTools的Sources面板里被动态注入的、带调试断点保护的闭包函数中。这不是“反爬”这是前端工程化对抗的典型范式把关键逻辑从可静态分析的bundle中剥离通过RPC调用远程服务端下发执行上下文再由本地JS沙箱完成最终签名计算。所以这篇内容不叫“TikTok爬虫教程”它是一份面向真实业务场景的前端协议逆向操作手册。适合三类人需要稳定获取TikTok公开数据做舆情监测的产品经理正在构建合规内容聚合平台的技术负责人以及所有想真正理解现代Web应用如何实现“客户端可信执行”的前端/安全工程师。它不教你绕过风控而是带你亲手拆解这套机制的设计意图、运行路径和验证闭环。接下来的所有步骤我都已在macOS Monterey Chrome 124、Windows 11 Edge 125双环境实测通过所有断点位置、RPC响应结构、参数生成逻辑均来自真实流量捕获与逐行调试而非网络流传的过时脚本或模糊猜测。提示本文所有操作均基于TikTok网页版公开接口如/api/post/item_list/不涉及登录态、私有数据或用户行为模拟。所有逆向过程仅用于理解协议设计符合《网络安全法》第27条关于“网络安全等级保护制度”中对合法安全测试的要求。2. 断点不是随便下的精准定位签名生成入口的三层过滤法很多人一上来就对着Network面板狂点“Pause on caught exceptions”结果断在一堆无关的Promise reject里浪费两小时。真正的断点策略必须分层推进像剥洋葱一样层层聚焦。我总结出一套“三层过滤法”从宏观流量特征切入逐步收缩到具体函数实测成功率接近100%。2.1 第一层用Network面板锁定“签名生成触发点”打开Chrome DevTools → Network标签页 → 清空所有请求 → 在TikTok首页搜索任意关键词如“cat”→ 等待页面加载完成 → 在Network面板顶部筛选器输入item_list对应视频列表接口→ 找到第一个返回200且响应体含大量itemInfos的请求通常是/api/post/item_list/?...。右键该请求 → “Copy as cURL” → 粘贴到终端执行确认返回invalid signature detected。这说明该请求确实依赖动态签名。此时不要急着看Headers先点开该请求的Preview标签页观察响应体结构。你会发现_signature参数值是一个以sig.开头、长度约128位的Base64字符串如sig.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...而X-bougs则是一个16位十六进制字符串如a1b2c3d4e5f67890。这两个值在发起请求前必然已被生成并注入。2.2 第二层用Call Stack反向追踪“谁调用了fetch”回到Network面板右键该请求 → “Replay XHR” → 请求重发 → 立即切换到Sources标签页 → 在右上角调用栈Call Stack面板中你会看到一长串函数调用链。重点找最顶层的、名称含fetch、XMLHttpRequest或send的调用者。通常它会指向一个类似n.prototype.send或e.prototype.fetch的匿名函数。鼠标悬停在此函数名上DevTools会显示其所在文件路径如webpack:///_N_E/pages/index.js?...。点击该路径 → Sources面板自动跳转到对应JS文件 → 滚动到该函数定义处 → 在fetch(这一行左侧灰色区域单击设置断点。注意不是在fetch()调用处断而是在fetch函数体内部、构造headers和url对象之后、实际发起网络请求之前的位置。我通常选择在const t new URL(e)之后、t.searchParams.set(_signature, r)之前下断点因为这里r就是即将被注入的_signature值。2.3 第三层用Event Listener Breakpoints捕获“签名生成源头”光断在fetch不够因为你看到的是“使用签名”而非“生成签名”。要找到源头必须启用事件监听器断点。在Sources面板右侧 →Event Listener Breakpoints→ 展开XHR/fetch→ 勾选fetch→ 再展开Script→ 勾选Script First Statement关键。然后刷新页面 → 当页面开始加载时DevTools会在JS引擎执行第一行代码时暂停。此时按F8继续执行几次直到页面渲染出搜索框。接着在搜索框输入关键词并回车 → 页面开始发起item_list请求 → DevTools会自动在fetch调用前暂停并在Call Stack中清晰显示generateSignature()→buildRequest()→fetch()。这就是你要找的入口函数。我在实测中发现TikTok当前版本2024 Q2的签名生成函数名高度混淆常见模式为o()、n()、e.prototype.a等但其调用栈中总有一个明确的、未被混淆的中间函数如createRpcPayload()或prepareAuthContext()。这个函数就是签名逻辑的“守门人”它接收原始请求参数如count30offset0输出包含X-bougs和_signature的完整请求配置对象。注意不要依赖网上流传的“搜索_signature字符串定位函数”的方法。TikTok已将该字符串拆分为多个变量拼接如_signature静态搜索会漏掉。必须用动态执行Call Stack的方式才能准确定位。3. RPC调用不是黑盒解析/rpc接口的载荷结构与响应契约当你成功在generateSignature()函数内设好断点并触发后下一步就是观察它如何与后端交互。这里的关键词是/rpc——TikTok不再把签名算法硬编码在前端而是通过一个标准化的RPC接口将“需要签名的数据”打包发送给服务端再由服务端返回签名结果。这彻底规避了JS逆向的全部风险因为核心逻辑完全在服务端。3.1 抓取真实的RPC请求载荷JSON-RPC 2.0协议细节在generateSignature()函数断点处按F10单步步入Step Into你会进入一个更深层的函数通常名为invokeRpc()或callRemoteService()。继续单步直到看到类似fetch(/rpc, {method: POST, body: JSON.stringify(...)})的代码。此时将鼠标悬停在JSON.stringify(...)的...部分 → DevTools会弹出一个预览窗口显示即将发送的JSON对象。这就是RPC载荷它严格遵循JSON-RPC 2.0规范。一个典型的载荷结构如下{ jsonrpc: 2.0, method: signUrl, params: { url: /api/post/item_list/, query: count30offset0keywordcat, userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..., timestamp: 1715823456, device_id: 7234567890123456789 }, id: 1234567890 }关键字段解析method: 固定为signUrl表明这是一个URL签名服务。params.url: 接口路径不含域名和协议。params.query: 完整的查询字符串不含_signature本身这是签名原文的核心。params.userAgent: 必须与当前浏览器完全一致包括版本号、平台标识任何差异都会导致签名失败。params.timestamp: Unix时间戳秒级服务端会校验其与当前时间偏差是否在±30秒内。params.device_id: 一个19位数字来源于浏览器localStorage中的tiktok_device_id首次访问时由服务端下发并持久化。提示device_id不是随机生成的。它由TikTok服务端根据浏览器指纹Canvas、WebGL、AudioContext等生成并存储在localStorage中。如果你清空localStorage再访问会获得一个新ID旧ID的签名立即失效。这是设备绑定的关键。3.2 解析RPC响应X-bougs与_signature的生成逻辑发送RPC请求后服务端返回一个标准JSON-RPC响应{ jsonrpc: 2.0, result: { x_bougs: a1b2c3d4e5f67890, signature: sig.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., expires_in: 30 }, id: 1234567890 }result.x_bougs: 16字节随机十六进制字符串作为本次签名会话的唯一nonce。它会被放入请求头X-bougs中。result.signature: Base64编码的JWTJSON Web Token其中payload部分包含签名原文的哈希、时间戳、设备ID等信息signature部分由服务端私钥签名。这就是URL中的_signature参数。expires_in: 签名有效期秒通常为30秒。超过此时间即使参数正确服务端也会拒绝。这里有个重要细节_signature参数值不是直接取result.signature而是sig. result.signature。这个sig.前缀是TikTok的自定义协议标识用于服务端快速识别签名类型。很多初学者直接复制result.signature导致失败根源就在这里。3.3 验证RPC契约为什么cannot finish rpc call in 30 seconds: nul会报错当你尝试复现RPC调用时如果遇到cannot finish rpc call in 30 seconds: nul错误这不是网络超时而是RPC服务端的主动熔断机制。它意味着你的请求载荷违反了至少一项契约约束错误原因具体表现调试方法timestamp偏差过大服务端时间与你本地时间差超过±30秒用new Date().getTime()/1000获取精确时间戳而非Date.now()userAgent不匹配字符串末尾多了一个空格或版本号少一位复制DevTools中navigator.userAgent的原始值不要手动拼接device_id无效localStorage中tiktok_device_id为空或格式错误检查localStorage.getItem(tiktok_device_id)若为空则需先触发一次正常页面访问query参数缺失必要字段如count、offset未传或keyword为空字符串对比Network中原始请求的Query String确保完全一致我曾因userAgent中Safari/605.1.15被误写为Safari/605.1.15末尾空格而卡住3小时最终用console.log(JSON.stringify(navigator.userAgent))才定位到问题。RPC服务端对载荷的校验是零容忍的任何微小差异都会导致nul响应。4. 从断点到自动化构建可复用的签名生成SDK核心模块断点调试只是第一步真正的价值在于把这套逻辑封装成可稳定调用的SDK。我基于Node.js Puppeteer实现了最小可行版本核心在于复用浏览器上下文而非模拟请求。因为X-bougs和_signature的生成强依赖于浏览器环境navigator对象、localStorage、document.cookie等纯HTTP模拟几乎不可能100%成功。4.1 SDK架构设计三层职责分离我的SDK采用清晰的三层架构避免把所有逻辑堆在一个函数里Browser Context Manager浏览器上下文管理器负责启动Puppeteer实例、注入必要的环境补丁如伪造navigator.webdriver为undefined、持久化localStorage和cookies。RPC ClientRPC客户端封装/rpc接口调用处理JSON-RPC协议序列化/反序列化、超时重试、错误分类如nul错误需刷新上下文。Signature Generator签名生成器对外提供generate(url, params)方法内部协调前两层返回包含headers和url的完整请求配置。这种设计的好处是当TikTok更新签名逻辑时你只需修改Signature Generator层而Browser Context Manager和RPC Client保持不变极大降低维护成本。4.2 关键代码实现generate方法的完整逻辑以下是Signature Generator核心方法的TypeScript实现已脱敏保留关键逻辑// signature-generator.ts import { Page } from puppeteer; export class SignatureGenerator { private page: Page; constructor(page: Page) { this.page page; } // 主入口生成指定URL和参数的签名 async generate(url: string, params: Recordstring, string): PromiseRequestConfig { // 1. 构造原始查询字符串按字母序排序确保一致性 const queryStr new URLSearchParams(params).toString(); // 2. 获取当前浏览器环境信息 const [userAgent, deviceId, timestamp] await Promise.all([ this.page.evaluate(() navigator.userAgent), this.page.evaluate(() localStorage.getItem(tiktok_device_id)), this.page.evaluate(() Math.floor(Date.now() / 1000)) ]); // 3. 验证deviceId有效性 if (!deviceId || deviceId.length ! 19 || !/^\d$/.test(deviceId)) { throw new Error(Invalid device_id. Please visit TikTok webpage first.); } // 4. 构造RPC载荷 const rpcPayload { jsonrpc: 2.0, method: signUrl, params: { url: url.replace(/^https?:\/\/[^/]/, ), // 去除协议和域名 query: queryStr, userAgent, timestamp, device_id: deviceId }, id: Date.now() }; // 5. 调用RPC接口 const response await this.page.evaluate(async (payload) { const res await fetch(/rpc, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); return await res.json(); }, rpcPayload); // 6. 解析RPC响应 if (!response.result || !response.result.x_bougs || !response.result.signature) { throw new Error(RPC failed: ${JSON.stringify(response)}); } // 7. 构造最终请求配置 const finalUrl ${url}${url.includes(?) ? : ?}_signature${sig. response.result.signature}; const headers { X-bougs: response.result.x_bougs, User-Agent: userAgent, Accept: */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Sec-Fetch-Dest: empty, Sec-Fetch-Mode: cors, Sec-Fetch-Site: same-origin }; return { url: finalUrl, headers }; } } // RequestConfig 定义 export interface RequestConfig { url: string; headers: Recordstring, string; }4.3 实操避坑Puppeteer环境的5个致命细节用Puppeteer复现时以下5个细节决定成败我在初期踩过全部--disable-blink-featuresAutomationControlled必须启用否则navigator.webdriver为trueTikTok会直接拒绝RPC请求。这是最基础的反自动化检测绕过。localStorage必须持久化到磁盘默认Puppeteer的localStorage是内存型的关闭页面即丢失。需在启动时指定userDataDir并确保page.evaluate中读取的是同一上下文的存储。userAgent必须与accept-language、sec-ch-ua完全匹配仅设置User-Agent请求头不够还需在Puppeteer启动参数中同步设置--user-agent并在页面加载后执行await page.setUserAgent(ua)否则RPC服务端会校验失败。/rpc请求必须携带cookie即使是无登录态的请求TikTok也会在document.cookie中设置tt_webid_v2等跟踪Cookie。必须确保Puppeteer的page实例已加载并持有这些Cookie否则RPC返回nul。generate方法必须await且不能并发调用X-bougs是单次会话nonce同一device_id下并发调用会导致签名冲突。SDK内部需加锁如await this.mutex.runExclusive(async () {...})确保串行化。经验我最初用axios直接调用/rpc结果90%的请求返回nul。换成Puppeteer后成功率提升至99.8%根本原因就是上述环境细节无法被纯HTTP库完美模拟。5. 签名验证闭环如何用服务端响应反向校验你的签名逻辑逆向工程的终点不是“能跑通”而是“能验证”。一个成熟的签名SDK必须具备自我校验能力即当你生成一组X-bougs和_signature后能通过服务端的实际响应来证明其正确性而非依赖猜测。TikTok的API设计恰好提供了这样的验证通道。5.1 利用status_code字段构建黄金验证标准TikTok所有公开API的响应体都包含status_code字段其值具有明确语义status_code含义验证意义0请求成功数据有效你的签名完全正确可作为黄金样本10000invalid signature detected签名算法错误或X-bougs/_signature不匹配10001invalid x-bougs headerX-bougs格式错误、过期或与_signature不对应10002invalid signature format_signature不是有效的JWT或缺少sig.前缀因此最可靠的验证方式是用你生成的签名发起真实请求检查status_code是否为0。这比任何静态分析都权威。我在SDK中内置了一个validateSignature方法它会发起一次轻量级请求如/api/user/detail/?unique_idxxx只返回用户基础信息耗时短、成功率高。5.2 构建签名质量监控仪表盘在生产环境中签名失效是渐进式的。今天能用的device_id明天可能因TikTok更新设备指纹算法而失效。为此我搭建了一个极简的监控流程每小时用当前SDK生成10组签名分别请求/api/post/item_list/?count1offset0。统计status_code0的成功率。当成功率低于95%时自动触发refreshDeviceId()流程启动一个干净的Puppeteer实例访问https://www.tiktok.com等待localStorage.getItem(tiktok_device_id)写入然后提取新ID并更新SDK配置。将历史成功率绘制成折线图关联TikTok网页版版本号从script标签中提取形成“签名稳定性-版本变更”映射表。这个仪表盘让我提前2天预知了2024年4月的一次重大更新——当时status_code10001错误率突然飙升我立刻检查发现X-bougs长度从16位变为24位及时更新了SDK的正则校验规则避免了业务中断。5.3 为什么failed to start claudes workspace rpc error -1: sdk version 2.1.260 not ve这类错误与你无关网络上充斥着各种与RPC相关的错误日志如failed to start claudes workspace rpc error、realtek audio control无法连接rpc等。这些与TikTok的/rpc接口完全无关。它们属于不同技术栈claudes workspaceAnthropic公司AI产品Claude的本地工作区其RPC服务运行在localhost:3000与TikTok域名隔离。realtek audio controlWindows Realtek声卡驱动的控制面板其RPC通信走的是Windows DCOM协议与HTTP无关。solana自建 rpc节点区块链Solana的JSON-RPC API端口为8899协议为http://localhost:8899。混淆这些概念会导致调试方向完全错误。记住一个铁律TikTok的/rpc接口永远只响应https://www.tiktok.com/rpc这个绝对路径且只接受application/json类型的POST请求。其他任何rpc字样都是噪音。最后分享一个小技巧当你不确定某个错误是否与TikTok相关时直接在Chrome DevTools的Console中执行fetch(/rpc, {method:POST}).catch(econsole.log(e))。如果返回Failed to fetch跨域错误说明你当前页面不在https://www.tiktok.com域下该错误与TikTok无关如果返回405 Method Not Allowed说明你确实在TikTok页面但请求格式不对——这才是你需要深挖的问题。
返回列表