本文详细解析了前端JavaScript检测浏览器开发者工具是否打开的三种主要方法:重写toString()、debugger断点机制以及窗口大小检测。文章分析了各方案的实现原理、代码示例及局限性,如toString仅能捕获状态切换,debugger可能被Deactive breakpoint绕过,窗口大小检测无法识别独立窗口模式。
JS检测浏览器开发者工具是否打开的方法详解
在某些业务场景下,我们需要判断用户是否开启了浏览器的开发者工具。例如在前端反爬虫策略中,若检测到控制台已开启,则将其视为潜在爬虫用户并采取相应限制措施。本文将深入探讨几种通过前端JavaScript检测开发者工具开启状态的技术方案。
一、重写toString()方法
一、重写toString()方法
针对Chrome、Firefox等浏览器,当控制台输出对象时,会保留该对象的引用。每次打开开发者工具查看该对象时,浏览器会重新调用该对象的toString()方法,并将返回结果打印在控制台(Console Tab)中。
基于此特性,我们可以创建一个对象并重写其toString()方法。在页面初始化时将该对象打印至控制台(此时假设控制台未开启)。当用户后续打开开发者工具查看该对象时,会触发toString()方法的调用,从而捕获到用户打开控制台的行为。
以下示例展示了当Chrome用户的开发者工具状态由关闭转为打开时,该动作会被捕获并执行回调函数:
<html>
<head>
<title>console detect test</title>
</head>
<body>
<script>
/**
* 控制台打开的时候回调方法
*/
function consoleOpenCallback(){
alert("CONSOLE OPEN");
return "";
}
/**
* 立即运行函数,用来检测控制台是否打开
*/
!function () {
// 创建一个对象
let foo = /./;
// 将其打印到控制台上,实际上是一个指针
console.log(foo);
// 要在第一次打印完之后再重写toString方法
foo.toString = consoleOpenCallback;
}()
</script>
</body>
</html>
效果演示如下:

需要注意的是,此方法仅在首次打开控制台时触发。若用户在已开启控制台的窗口中粘贴网址访问,或在该页面已开启控制台的情况下刷新页面,则不会触发检测。
尽管该方法较为巧妙,但缺乏通用性,且仅能捕获开发者工具从关闭到打开的状态转移过程,存在一定局限性。
二、利用debugger语句
二、利用debugger语句
类似于代码调试中的断点,当浏览器开启开发者工具(即Debug模式)并遇到debugger语句时,程序执行会暂停:

此时用户需点击蓝色的“Resume script execution”按钮以继续执行,这一过程会产生时间差。通过判断该时间差是否超过设定阈值,即可推断开发者工具是否开启。该方法不会造成误判,未开启工具时debugger语句不会暂停,因此通用性较好。
以下是使用debugger语句检测开发者工具开启状态的示例:
<html>
<head></head>
<body>
<script>
function consoleOpenCallback() {
alert("CONSOLE OPEN");
}
!function () {
const handler = setInterval(() => {
const before = new Date();
debugger;
const after = new Date();
const cost = after.getTime() - before.getTime();
if (cost > 100) {
consoleOpenCallback();
clearInterval(handler)
}
}, 1000)
}();
</script>
</body>
</html>
效果演示如下:

然而,上述代码存在一个严重缺陷:若用户在执行到debugger语句时察觉异常,未点击“Resume script execution”而是直接退出页面,则无法检测到本次开启行为,导致实际结果与预期不符。这类似于电影中踩中地雷后及时察觉并缩脚以保命,若用户识别出这是检测代码并退出,使用其他手段绕过,则检测功能将失效。
使用此方法时需注意,当程序卡在debugger语句处,用户可见附近的代码。若用户具备一定经验,可轻易识破其中逻辑。因此,需隐藏真实目的,例如隐藏debugger语句并对代码进行混淆以增加阅读难度。关于如何隐藏debugger语句前后的逻辑,可参考以下网站(截至2018-7-4 23:12:17有效):
http://app2.sfda.gov.cn/datasearchp/gzcxSearch.do?formRender=cx&optionType=V1
https://www.qimai.cn/
使用此方案时需注意,debugger语句可能不会暂停。例如在Chrome浏览器的Source面板中,若选择“Deactive breakpoint”,则debugger语句不会暂停。

若该按钮被激活,测试上述网页会发现检测代码失效,因为debugger语句并未暂停执行。
此外,debugger语句还可用于反调试。例如设置每秒触发一次debugger,使调试者疲于应付或增加其覆盖JS的成本。上述几个网站即采用此策略。
以下是使用debugger语句进行JS反调试的简单示例:
<html>
<head>
<title>Anti debug</title>
</head>
<body>
<script>
!function () {
setInterval(() => {
debugger;
}, 1000);
}();
</script>
</body>
</html>
效果演示如下:

一个实际案例是网站:http://jxw.uou0.com/的JS检测脚本,其针对不同情况采取不同的反调试策略。
注意,复现此场景需粘贴视频地址解析后才会加载检测脚本。例如可尝试解析该视频:http://film.qq.com/film/p/topic/thwjlxby/index.html。
当未打开开发者工具进行解析,随后打开开发者工具时,将使用此检测方式:
!function() {
var timelimit = 50;
var open = false;
setInterval(function() {
var starttime = new Date();
debugger ;if (new Date() - starttime > timelimit) {
open = true;
window.stop();
$("#loading").hide();
$("#a1").remove();
$("#error").show();
$("#error").html("\u7cfb\u7edf\u68c0\u6d4b\u975e\u6cd5\u8c03\u8bd5\u002c\u8bf7\u5237\u65b0\u91cd\u8bd5\u0021")
} else {
open = false
}
}, 500)
}();
由于该网站提供VIP视频免费解析,一旦检测到有人打开开发者工具进行调试,便会移除解析好的视频,并弹出提示框:

若已打开开发者工具再粘贴地址进行视频解析,则会触发无限debugger。
应对上述脚本最简单的方法是将Chrome浏览器设定为“Deactive breakpoint”,此时上述脚本将失效,但同时也影响自身调试。用于反调试确实能增加对方成本,较好的方法是使用Fiddler修改网页返回内容,过滤掉debugger语句以破解此套路。
三、检测窗口大小
三、检测窗口大小
检测窗口大小相对简单,需明确两个概念:窗口的outer大小和inner大小:
window.innerWidth / window.innerHeight:表示可视区域的宽高,其中window.innerWidth包含纵向滚动条的宽度,window.innerHeight包含水平(横向)滚动条的宽度。
window.outerWidth / window.outerHeight:在innerWidth和innerHeight的基础上加上工具栏的宽度。
关于窗口大小检测,此处不自行编写示例,已有专门针对此功能的库:https://github.com/sindresorhus/devtools-detect。该库拥有数百个Star,质量较高。代码较为简单,GitHub上有使用说明。此处仅对其核心代码进行分析,并附上本人对该库核心代码的分析:
/* eslint-disable spaced-comment */
/*!
devtools-detect
Detect if DevTools is open
https://github.com/sindresorhus/devtools-detect
by Sindre Sorhus
MIT License
comment by CC11001100
*/
(function () {
'use strict';
var devtools = {
open: false,
orientation: null
};
// inner大小和outer大小超过threshold被认为是打开了开发者工具
var threshold = 160;
// 当检测到开发者工具后发出一个事件,外部监听此事件即可,设计得真好,很好的实现了解耦
var emitEvent = function (state, orientation) {
window.dispatchEvent(new CustomEvent('devtoolschange', {
detail: {
open: state,
orientation: orientation
}
}));
};
// 每500毫秒检测一次开发者工具的状态,当状态改变时触发事件
setInterval(function () {
var widthThreshold = window.outerWidth - window.innerWidth > threshold;
var heightThreshold = window.outerHeight - window.innerHeight > threshold;
var orientation = widthThreshold ? 'vertical' : 'horizontal';
// 第一个条件判断没看明白,heightThreshold和widthThreshold不太可能同时为true,不论是其中任意一个false还是两个都false取反之后都会为true,此表达式恒为true
if (!(heightThreshold && widthThreshold) &&
// 针对Firebug插件做检查
((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)) {
// 开发者工具打开,如果之前开发者工具没有打开,或者已经打开但是靠边的方向变了才会发送事件
if (!devtools.open || devtools.orientation !== orientation) {
emitEvent(true, orientation);
}
devtools.open = true;
devtools.orientation = orientation;
} else {
// 开发者工具没有打开,如果之前处于打开状态则触发事件报告状态
if (devtools.open) {
emitEvent(false, null);
}
// 将标志位恢复到未打开
devtools.open = false;
devtools.orientation = null;
}
}, 500);
if (typeof module !== 'undefined' && module.exports) {
module.exports = devtools;
} else {
window.devtools = devtools;
}
})();
缺点分析:
1. 使用window属性检查大小可能存在浏览器兼容性问题,因非专业前端仅测试了Chrome和Firefox,结果无误。
2. 此方案仍有漏洞。以Chrome浏览器为例,开发者工具窗口有四种选项:单独窗口、靠左、靠下、靠右。

靠左、靠右、靠下会占用当前窗口部分空间,此种情况可被检测到;但独立窗口不占用打开网页窗口的空间,因此无法检测。可前往此页面验证:https://sindresorhus.com/devtools-detect/。
四、总结
四、总结
本文介绍了几种检测方式,各有利弊。以下是对各方案缺点的简要总结:
重写toString():仅能捕获开发者工具从关闭状态向打开状态转移的过程。
debugger语句:当勾选Chrome浏览器的“Deactive breakpoint
”时,debugger语句不会暂停,导致无法捕获。
检测窗口大小:当开发者工具以独立窗口模式打开时,无法检测到。