先认清对手:a_bogus 是 VMP 保护的
抖音 web 端的签名参数从 _signature 到 X-Bogus 再到现在的 a_bogus,防护强度一路升级。当前版本的生成代码在 webmssdk.js 里,整个文件跑在字节自研的 JS 虚拟机上——你看到的不是 JS 代码,而是一堆字节码和一个巨大的 switch-case 解释器。
这意味着"读源码理解算法"这条路基本被堵死:不是不能,是投入产出比极低。工程上成熟的打法是整段抠出 + 补环境,让这段 VMP 代码在 Node 里原样跑起来,我们只管调它的导出函数。
第一步:确认签名入口
webmssdk 加载后会在 window 上挂出入口,字节系的经典名字是 byted_acrawler:
// Console 里验证
window.byted_acrawler.sign({ url: "https://www.douyin.com/aweme/v1/web/aweme/post/?device_platform=webapp&aid=6383&..." })
// 返回完整的 a_bogus 字符串不同页面版本入口名字可能不同(有的版本挂在更深的命名空间下),不确定就用 hook 定位:
const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(m, u) {
if (u.includes('a_bogus=')) debugger;
return _open.apply(this, arguments);
};断下后看调用栈里最底层(最接近 SDK 内部)的那帧,它读取的那个函数就是入口。
第二步:抠出 SDK,一个字节都别动
这是整个方案里最容易翻车的一步,要点:
- 完整保存 webmssdk.js。不要格式化、不要删"无用"代码、不要调整顺序。VMP 代码里有大量基于执行时序的自校验,格式化工具改一个换行都可能导致输出不同。
- 找到它的加载方式。有的版本是独立 script 标签,有的是嵌在主 chunk 里通过 eval 释放。后者要把 eval 的字符串参数抠出来。
- 记录初始化时序。SDK 可能在
DOMContentLoaded后才完成初始化,Node 里要手动触发对应流程,或者轮询window.byted_acrawler直到可用。
第三步:补环境——真正的战场
VMP 代码对环境的探测非常细致,这是补环境的清单(按踩坑频率排序):
global.window = global;
window.navigator = {
userAgent: UA, // 必须和后续请求的 ua 一致
platform: 'Win32',
webdriver: false, // 必检项
languages: ['zh-CN', 'zh'],
plugins: { length: 3 }, // 空 plugins 是无头浏览器典型特征
hardwareConcurrency: 8,
deviceMemory: 8,
};
window.document = {
cookie: '',
referrer: 'https://www.douyin.com/',
createElement: (tag) => tag === 'canvas' ? fakeCanvas : {},
addEventListener: () => {},
hidden: false,
visibilityState: 'visible',
};
window.location = new URL('https://www.douyin.com/');
window.screen = { width: 1920, height: 1080, availWidth: 1920, availHeight: 1040, colorDepth: 24 };
window.performance = { now: () => Date.now() - START_TIME }; // 返回值要单调递增且合理
window.requestAnimationFrame = (cb) => setTimeout(cb, 16);
window.XMLHttpRequest = function() {}; // SDK 内部可能发探测请求,要拦几个要命的细节:
performance.now()不能返回 0 或定值。SDK 会多次调用计算代码执行耗时,全 0 直接判定异常环境。用真实流逝时间。- canvas 要有合理的 toDataURL 返回。不需要真渲染,从真实浏览器采一个 dataURL 固定返回即可,但要和 ua 声称的平台匹配。
- 函数 toString 检测。SDK 会
Function.prototype.toString.call(someNativeFunc)验证是不是原生代码。你 hook 过的函数要用 toString 补丁伪装回function xxx() { [native code] }。
第四步:对拍验证
环境补没补对,唯一的裁判是对拍:
// Node 侧
const sig = window.byted_acrawler.sign({ url: TEST_URL });
console.log(sig);同一个 TEST_URL,浏览器 Console 里算一次,Node 里算一次。注意 a_bogus 含时间戳和随机数,两次结果不一样是正常的——对拍看的不是相等,而是"用 Node 生成的 a_bogus 去发真实请求,接口放不放行"。放行 = 环境合格;返回验证页 = 继续补。
精髓:msToken 和 ttwid 别忘了
a_bogus 只是请求签名,抖音 web 端还有两个配套参数:
msToken:由另一个 SDK(或响应 Set-Cookie)下发,风控令牌,请求里要原样带。它有有效期,过期要重取。ttwid:设备级 cookie,由ttwid.bytedance.com注册接口下发,长期有效,是设备归因的核心。协议池里每个"设备"固定一个 ttwid。
三者关系:ttwid 定设备,msToken 定会话风险等级,a_bogus 定单次请求合法性。只模拟 a_bogus 而忽视前两个,请求照样被拦。
总结
- a_bogus 是 VMP 保护,放弃读算法,走"整段抠出 + 补环境"
- 抠代码原样保存,格式化是隐形杀手
- 补环境三大考点:performance 计时、canvas 指纹、函数 toString 伪装
- 对拍标准是"真实请求放行",不是和浏览器结果相等
- msToken、ttwid 与 a_bogus 三位一体,缺一不可