在某些情况下我们需要检测当前用户是否打开了浏览器开发者工具,比如前端爬虫检测,如果检测到用户打开了控制台就认为是潜在的爬虫用户,再通过其它策略对其进行处理。本篇文章主要讲述几种前端JS检测开发者工具是否打开的方法。
一、重写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
类似于代码里的断点,浏览器在打开开发者工具时(对应于代码调试时的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>
效果:
但是上面的代码有一个很严重的bug,就是在执行到debugger那一行的时候如果用户发现了猫腻没有点按resume script execution按钮,而是直接退出页面的话,那么将不能检测到本次的打开开发者工具行为,实际结果与预期不符,我认为这是严重bug,就像电影里演的不小心踩到地雷及时察觉不抬脚就还有活命机会,到了debugger标签我察觉到这是检测控制台是否打开的代码我退出然后使用其它手段绕过它,那我可能做了一个假功能。
有一个需要注意的地方就是使用此方法的时候当卡在debugger标签的时候,用户是能够看到debugger标签附近的代码的,如果是有经验的用户一眼就能看出里面的道道,所以要想办法隐藏一下真实目的,比如将debugger标签隐藏,并且对代码进行混淆尽量增加阅读难度,关于如何隐藏debugger标签前后的逻辑,可以参考这几个网站:(当前2018-7-4 23:12:17有效)
http://app2.sfda.gov.cn/datasearchp/gzcxSearch.do?formRender=cx&optionType=V1
使用此种方案的话可能有个需要注意的点就是debugger是有可能不会暂停的,比如Chrome浏览器的source面板可以选择在debugger语句时不暂停:
如果这个按钮被点亮,再测试上面的网页就会发现很悲剧检测代码失效了,因为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("u7cfbu7edfu68c0u6d4bu975eu6cd5u8c03u8bd5u002cu8bf7u5237u65b0u91cdu8bd5u0021") } 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和ff是没有问题的。
2. 此方案还是有漏洞的,就拿Chrome浏览器来说,开发者工具窗口有四个选项:单独窗口、靠左、靠下、靠右。
靠左、靠右、靠下都会占用当前窗口的一些空间,这种情况会被检测到,但是独立窗口并不会占用打开网页窗口的空间,所以这种情况是检测不到的,可去此页面进行验证:https://sindresorhus.com/devtools-detect/。
四、总结
本文介绍了几种检测方式,其各有利弊,下面是对其缺点的一个简单的总结:
重写toString():只能捕获到开发者工具从关闭状态向打开状态转移的过程
debugger标签:当勾选了Chrome浏览器的Deactive breakpoint ,debugger标签不会暂停,将捕获不到
检测窗口大小:当开发者工具是以独立窗口打开的时候不能检测到
相关资料:
1. Find out whether Chrome console is open
.