我们经常有这样的需求:想知道用户之前有没有访问过某个网址。有没有什么方法或技术能实现这一点呢?
初步探索
注意到,在大部分浏览器默认设置里,用户访问过的链接和没访问过的链接颜色是不同的,如下图:
即用户访问过的链接,computed color默认为紫色(或其他在CSS中指定的颜色):
而没访问过的链接,computed color默认为蓝色(或其他在CSS中指定的颜色):
那是不是说,我们可以在页面上加上我们感兴趣的链接,然后用JavaScript取得这些链接文本实际的颜色,即可知道用户是否访问过指定网址呢?
这个方法真的有效过。2010年有一篇安全文章上即介绍了这种方法,并将这类方法称为“历史嗅探”(history sniffing)。
遗憾的是,各大浏览器厂商都已经注意到了这个问题,根据我的测试,目前最新的浏览器中都对这个问题进行了防范,获取超链接的Computed Style时,无论这个链接是否被访问过,取得的颜色都是未访问过时的那种颜色。
看起来根据颜色获取的方案目前行不通了,不过神奇的是,现在我们有了另外一种方案。
requestAnimationFrame
Context Information Security公司最近出了一份名为《PIXEL PERFECT TIMING ATTACKS WITH HTML5》的报告,其中提到了一种很有创意的方案:使用HTML5中的requestAnimationFrame,根据浏览器渲染已访问过及未访问过的链接的时间差,判断指定链接是否访问过。
现代浏览器绘图时每一帧的流程大致如下图所示:
大致流程为:JS修改某个元素的样式,浏览器重新计算对应元素的外观及位置,然后将它们绘制出来,这个过程即是一帧。而requestAnimationFrame的作用则是可以注册一个函数,在下一帧开始绘制之前进行调用。
requestAnimationFrame的初衷是让开发人员可以更好地管理动画,绘制更平滑的动画,如这篇博客中所说的。不过,这个接口也让获取不同元素的渲染时间成为了可能。
工作原理
在开始之前,我们需要了解的是浏览器是如何渲染访问过的链接和未访问过的链接的。
当浏览器渲染一个页面时,浏览器必须区分出某个链接是否曾访问过。每个浏览器都有一个记录访问过的链接的数据库,此时它要做的就是从这个数据库中查询
指定的URL是否存在。
IE与Firefox中,如果链接已经渲染到页面上了,查询还没完成,浏览器会先使用“未访问过”的样式来渲染,查询结果返回时,如果指定链接是已访问过,那么浏览器就重绘指定的链接。而这个“重绘”是需要时间的,可以使用requestAnimationFrame来监测。
Chrome浏览器和Firefox、IE不同,它会一直等到数据库URL查询完成才将链接渲染到屏幕上。
除了初始渲染之外,使用JavaScript修改链接的href也有可能引发浏览器重绘。测试显示,在Firefox中,修改一个链接的href,如果改变了它的“已访问”状态,则会引发重绘。但在IE中这不能工作,一个链接一旦创建,改变href永远也不会同时改变它的“已访问”状态。
Chrome中有点例外,只改变href并不会引发重绘,不过,如果在改变href的同时也“触碰”一下链接的样式(但不修改),则当新href需要改变“已访问”状态时,浏览器会重绘对应的链接。
所谓的“触碰”指的是类似这样的操作:
|
1
2
3
4
5
6
7
8
9
10
|
href="http://www.g**o*ogle.com" id="link1">############a>
var el = document.getElementById('link1'); el.href = 'http://www.y*a*ho*o.com'; // below lines are required for Chrome to force style update el.style.color = 'red'; el.style.color = ''; script> |
简单来说,基本原理就是这样:创建链接,改变它的href,使用requestAnimationFrame来监测接下来若干帧的耗时,判断是否发生了重绘,如果发生了重绘,说明指定链接的“已访问”状态发生了变化,即可判断出指定链接是否被访问过。
当然,实际操作过程中还有不少问题需要考虑,比如,浏览器渲染通常都非常快,重绘的时间可能也会非常短,导致完全无法区分。解决方案主要有两个,一是增加链接数,创建多个A链接,指向同一个URL,需要时使用JS同时改变这些A链接的href属性为另一个值。另一个方案是给元素加一些耗时的样式,比如文字阴影,并且让模糊半径尽可能大,这样在重绘时需要的时间就会多很多了。
实践
我写了一个针对Chrome浏览器的demo,你可以使用Chrome访问
浏览器历史嗅探 demo,仅支持 Chrome,在
Mac 10.8.4/Chrome 28.0、
Windows XP SP3/Chrome 28.0、
Ubuntu 12.4/Chrome 28.0
上测试通过。
关于:
function checkIsLinkVisted(url, callback) {
var i_count = 10;
var times = [];
// $("#test-area a").each(function () {
// this.href = known_visited_url;
// });
function check(time) {
var delay = parseInt(time - last_time);
times.push(delay);
last_time = time;
if (i_count > 0) {
requestAnimationFrame(check);
} else {
checkEnd();
}
i_count--;
}
function checkEnd() {
var large_time_count = 0;
var large_time = 60;
for (var i = 0; i
if (times[i] > large_time) large_time_count++;
}
console.log(times);
if (min_large_time == -1) {
min_large_time = large_time_count;
if (min_large_time
current_is_visited = true;
} else if (large_time_count >= min_large_time) {
current_is_visited = !current_is_visited;
}
$("#links").append("
" + (current_is_visited ? "[v]" : "[ ]") + " " + "" + "" + times.join(", ") + "" + ""); if (callback && typeof callback == "function") callback();
}
setTimeout(function () {
$("#test-area a").each(function () {
this.href = url;
this.style.color = "red";
this.style.color = "";
});
requestAnimationFrame(check);
}, 500);
}
function initTestArea() {
var htmls = [];
for (var i = 0; i
htmls.push("