文章

Boss 直聘反调试技术深度解析 —— 从 F12 禁用到 8 种 DevTools 检测

深入分析 Boss 直聘网站的前端反调试技术体系,涵盖键盘拦截、DevTools 检测的 8 种探测器原理、惩罚机制,以及完整的反爬架构

Boss 直聘反调试技术深度解析 —— 从 F12 禁用到 8 种 DevTools 检测

在 Boss 直聘网站上,你可能遇到过这样的情况:按 F12 毫无反应,右键菜单被禁用,即使通过浏览器菜单强行打开 DevTools,页面也会不断刷新并触发安全验证。这并非简单的 JavaScript 小把戏,而是一套工业级的前端反调试防护体系

本文将从技术原理层面,逐一拆解这套防护体系的实现方式。


一、快捷键与右键拦截

最外层的防护是阻止用户打开 DevTools 的入口。实现方式非常直接——监听键盘和右键事件,拦截所有已知的 DevTools 快捷键。

1.1 键盘快捷键拦截

通过 keydown 事件监听,拦截以下快捷键:

快捷键功能keyCode
F12打开 DevTools123
Ctrl+Shift+I审查元素73
Ctrl+Shift+J控制台74
Ctrl+U查看源代码85
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
document.addEventListener('keydown', function(e) {
  // F12
  if (e.keyCode === 123) {
    e.preventDefault();
    return false;
  }
  // Ctrl+Shift+I (审查元素)
  if (e.ctrlKey && e.shiftKey && e.keyCode === 73) {
    e.preventDefault();
    return false;
  }
  // Ctrl+Shift+J (控制台)
  if (e.ctrlKey && e.shiftKey && e.keyCode === 74) {
    e.preventDefault();
    return false;
  }
  // Ctrl+U (查看源代码)
  if (e.ctrlKey && e.keyCode === 85) {
    e.preventDefault();
    return false;
  }
});

1.2 右键菜单禁用

1
2
3
4
document.addEventListener('contextmenu', function(e) {
  e.preventDefault();
  return false;
});

这一层防护能阻止大约 90% 的普通用户,但对有经验的开发者来说,通过浏览器菜单或命令行参数仍然可以打开 DevTools。因此,真正的核心防护在第二层。


二、DevTools 开启检测的 8 种技术手段

Boss 直聘大概率使用了 disable-devtool(GitHub 3200+ stars,国人开发,npm 周下载量极高)或类似的自研方案。该库内置了 8 种探测器(Detector),以定时轮询的方式同时运行,任一触发即判定 DevTools 已打开

2.1 RegToString 检测(正则 toString 劫持)

1
2
3
4
5
6
const reg = /./;
reg.toString = function() {
  devtoolsDetected = true;
  return '';
};
console.log(reg);

原理console.log() 在 DevTools 关闭时是空操作——浏览器不会真正序列化参数,因为没有面板需要渲染。但当 DevTools 打开时,控制台面板需要显示输出内容,浏览器会调用对象的 toString() 方法进行序列化。通过重写 toString(),我们可以在其中设置标记,一旦被调用就说明 DevTools 正在运行。

2.2 DefineId 检测(DOM 元素 id 属性)

1
2
3
4
5
6
7
const element = document.createElement('div');
Object.defineProperty(element, 'id', {
  get: function() {
    devtoolsDetected = true;
  }
});
console.log(element);

原理:当 DevTools 的 Elements 面板处于打开状态时,它会主动读取 DOM 元素的属性来渲染 DOM 树。通过 Object.defineProperty 给元素的 id 属性设置 getter,当 DevTools 试图读取该属性时就会触发 getter 函数。

2.3 窗口尺寸检测(Size Detection)

1
2
3
4
5
6
7
8
9
setInterval(function() {
  const threshold = 160;
  const widthDiff = window.outerWidth - window.innerWidth;
  const heightDiff = window.outerHeight - window.innerHeight;

  if (widthDiff > threshold || heightDiff > threshold) {
    devtoolsDetected = true;
  }
}, 200);

原理:当 DevTools 以侧边栏或底部面板的方式内嵌在浏览器窗口中时,它会”挤占”页面的可视区域。window.outerWidth 是浏览器窗口的总宽度,window.innerWidth 是页面内容区的宽度,两者的差值在正常情况下很小(只有滚动条和边框的宽度),但 DevTools 打开后差值会显著增大(通常超过 160px)。

注意:这种检测对独立窗口(Undock into separate window)模式无效,因为 DevTools 不再占用页面空间。

2.4 DateToString 检测

1
2
3
4
5
6
const date = new Date();
date.toString = function() {
  devtoolsDetected = true;
  return '';
};
console.log(date);

原理与 RegToString 完全相同,只是载体换成了 Date 对象。多种对象类型同时检测,是为了提高覆盖率——不同浏览器对不同对象的序列化行为可能存在差异。

2.5 FuncToString 检测(Function.prototype.toString)

1
2
3
4
5
6
const func = function() {};
func.toString = function() {
  devtoolsDetected = true;
  return '';
};
console.log(func);

同样利用了 console.log 在 DevTools 打开时序列化对象的特性。Function 对象作为又一个检测维度,进一步增强了探测的可靠性。

2.6 Debugger 断点检测(无限 Debugger)

这是最核心的检测手段,也是用户看到“页面不断刷新”的直接原因。

1
2
3
4
5
6
7
8
9
10
11
12
setInterval(function() {
  const startTime = performance.now();

  // 动态生成 debugger 语句
  (function(){}).constructor('debugger')();

  const endTime = performance.now();
  // 正常执行 < 1ms,被断点暂停会 >> 100ms
  if (endTime - startTime > 100) {
    devtoolsDetected = true;
  }
}, 200);

原理debugger 语句在 DevTools 打开时会触发断点暂停(如果 Sources 面板处于激活状态)。通过 performance.now() 记录执行前后的时间差:

  • DevTools 关闭时:debugger 被忽略,执行耗时 < 1ms
  • DevTools 打开时:debugger 触发断点暂停,即使用户立即点击继续,耗时也会远超 100ms

使用 (function(){}).constructor('debugger')() 而非直接写 debugger 是为了避免被静态分析工具或代码压缩器移除——动态生成的代码在语法树中不可见。

每 200ms 执行一次检测,一旦检测到就立即触发 location.reload() 刷新页面,这就是用户看到”不断刷新”的原因。

2.7 Performance 检测(大数据 console.log 计时)

1
2
3
4
5
6
7
8
9
10
setInterval(function() {
  const startTime = performance.now();
  console.log(new Array(1000000).fill('x').join(''));
  console.clear();
  const endTime = performance.now();

  if (endTime - startTime > 10) {
    devtoolsDetected = true;
  }
}, 500);

原理:向控制台输出一个超大字符串,然后立即清除。当 DevTools 关闭时,console.log 几乎零开销(浏览器直接丢弃输出);当 DevTools 打开时,控制台面板需要实际渲染这段巨大的文本,耗时会从微秒级飙升到数十毫秒。通过时间差即可判断 DevTools 是否在运行。

2.8 DebugLib 检测(第三方调试工具)

1
2
3
4
5
if (window.eruda || window.VConsole ||
    document.querySelector('#eruda') ||
    document.querySelector('.vc-switch')) {
  devtoolsDetected = true;
}

原理ErudavConsole 是两款流行的移动端调试工具,常被用于在手机浏览器上模拟 DevTools。这种检测通过检查全局变量和特定 DOM 元素来判断是否加载了这些工具。


三、检测后的惩罚机制

一旦上述 8 种探测器中的任何一个被触发,网站会立即执行惩罚操作。常见的惩罚手段包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 关闭当前页面
window.close();

// 2. 跳转到空白页
window.location.href = 'about:blank';

// 3. 刷新页面(最常见,用户看到的现象)
window.location.reload();

// 4. 清空整个页面内容
document.documentElement.innerHTML = '';

// 5. 触发安全验证(极验/网易易盾滑块验证)
triggerSecurityVerification();

// 6. 后端标记会话异常
// 通过 cookie 中的 __zp_stoken__ 等字段标记当前会话为可疑
// 后续请求会被要求完成验证码挑战

Boss 直聘采用的策略是组合惩罚:先刷新页面(location.reload()),如果用户反复触发,则升级为验证码挑战。这种渐进式惩罚既不会误伤正常用户(偶尔误触不会有太大影响),又能有效阻止持续调试行为。


四、Boss 直聘完整反爬体系架构

前端反调试只是 Boss 直聘整体反爬体系的冰山一角。根据公开的逆向分析资料,其完整防护体系包含至少 6 个层级:

层级技术说明
前端反调试disable-devtool 类库本文分析的 8 种检测 + 惩罚机制
动态 Token__zp_stoken__每天变化的 JS 代码动态生成 token,混淆后注入请求
代码混淆控制流平坦化 + 阿里系混淆将正常的代码逻辑打散为 switch-case 结构,极大增加逆向难度
环境检测浏览器指纹 + 环境一致性校验检测 navigator、screen、WebGL 等指纹是否与真实浏览器一致
验证码极验 + 网易易盾双重验证智能风控系统动态触发滑块/文字点选验证
通信加密MQTT + Protobuf聊天功能使用 WebSocket + Protobuf 编码,防止抓包分析

其中,__zp_stoken__ 是一个特别值得关注的机制。它不是一个静态 token,而是由每天动态变化的 JavaScript 代码生成的。这意味着即使你今天逆向出了 token 的生成逻辑,明天代码就会发生变化,需要重新分析。这种动态混淆策略大幅提高了自动化爬取的维护成本。


五、绕过思路简述

以下内容仅供技术学习和安全研究参考,请遵守相关法律法规和网站使用条款。

从纯技术角度,针对上述防护体系存在以下思路:

针对快捷键拦截:通过浏览器菜单(更多工具 > 开发者工具)或启动 Chrome 时添加 --auto-open-devtools-for-tabs 参数,绕过键盘事件拦截。

针对 DevTools 检测:使用 Chrome 的 Local Overrides 功能,将网站的 JavaScript 文件替换为去除检测逻辑的版本。或者在 DevTools 的 Sources 面板中,右键 debugger 断点选择 “Never pause here” 来跳过无限 debugger。

针对环境检测:使用 Playwright 或 Puppeteer 的 stealth 插件(如 puppeteer-extra-plugin-stealth),模拟真实浏览器指纹。

但需要强调的是,这些绕过手段本身也在被反制。现代反爬体系是一个持续对抗的过程,防守方和攻击方都在不断迭代升级。


六、总结 —— 一场成本博弈

Boss 直聘的反调试体系本质上是一场成本博弈

  1. 快捷键/右键拦截 → 零成本阻止 90% 的好奇用户
  2. 8 种并行探测器 → 每 200ms 多维度轮询,覆盖 console 序列化、窗口尺寸、断点计时、渲染耗时等多个检测面
  3. 检测即惩罚 → 刷新页面 / 清空 DOM / 触发验证码,让调试过程极度痛苦
  4. 动态 Token + 代码混淆 → 即使突破前端防护,后端接口还有独立的反爬层
  5. 持续迭代 → 检测代码定期更新,token 生成逻辑每日变化

这套体系的设计哲学不是”绝对不可破解”,而是让破解的成本高于收益。对于绝大多数爬虫开发者来说,面对这样的防护体系,要么选择更换数据源,要么接受极高的维护成本——而这正是平台想要达到的效果。

本文由作者按照 CC BY 4.0 进行授权