对于数据统计分析或者数据挖掘而言,用户是个非常重要的维度,也是统计分析能落地的基础。一般而言,咱们追踪或者识别一个用户的首选方案是 userID,大多数公司的产品都会要求用户注册、登录操作,都存在一个类似 UMC 的数据库,管理和标示所有的用户。但这有个前提条件,就是你所在的公司业务必须以闭环为主(比如 qq、微信、淘宝等)。如果产品没有形成闭环,用户就不会主动去注册、登录,那上面通过 userID 数据库来管理、追踪用户行为的方案就不行了。比如BBS站点或者广告联盟都会非常想要一种技术方式可以在网络上精确定位到每一个个体,这样可以通过收集这些个体的数据,通过分析后更加精准的去推送广告(精准化营销)或其他有针对性的一些活动。当用户访问一个网站时,网站生成一个含有唯一标示符(UUID)的信息,并通过这个信息将用户所有行为(浏览了哪些页面?搜索了哪些关键字?对什么感兴趣?点了哪些按钮?用了哪些功能?看了哪些商品?把哪些放入了购物车等等)关联起来。那这种情况下有没有可能有其它的技术方案去管理追踪这种游客态用户呢?
答案或许很多同学会回答用 cookie。是的,对于游客态用户而言,常用的身份识别方案就是使用 cookie,技术实现难度小,成本相对很低廉。那是不是使用 cookie 就万事大吉了呢?准确性、稳定性、可辨识性怎么样?下面咱们就来深入探讨下 cookie 追踪用户的利弊及其发展与移动互联网时代下用户身份识别面临的新问题。
先上一张图,可以看到大部分流行的方法还是基于Cookie,只是这些 Cookie 会稍有不同,本文会按照整张图的脉络来一一介绍各种 cookie 及其利与弊。
为什么会有 HTTPCookie 呢?因为HTTP协议是无状态的,即服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么。 所以Cookie就是用来绕开HTTP的无状态性的“额外手段”之一。服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态。
Cookie是由服务器端生成(webserver或者cgi),response 给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie)。
Cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
由于在HTTP请求中的Cookie是明文传递的,所以安全性成问题。(除非用HTTPS)
Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。
浏览器安全策略不允许种植 cookieID、用户清除 cookieID,不同的浏览器也会生成不同的 cookieID,大量的爬虫也可能带上随机 cookieID
识别不准确:浏览器安全策略不允许种植 cookieID、用户清除 cookieID,不同的浏览器生成不同的 cookieID,大量的爬虫带上随机 cookieID
在客户端Cookie里保存数据是不稳 定的,因为用户可能随时会清除掉浏览器的Cookie,在这种情况下,一般的解决方案是重新向服务器端发送一个请求,以获得一个新的HTTP Cookie数据,并将其保存,就一般的交互需求而言,这是没有问题的。但是,倘若我的需求是:要求恢复到原来的Cookie里保存数据持久的追踪用户的行为呢?这种情况,倘若服务器端没有做特殊的处理的话,显然是很难实现的,这里就该 Flash Cookie 登场了。
FlashCookie是由FlashPlayer控制的客户端共享存储技术,它具备以下特点:
类似 HTTPCookie,FlashCookie利用SharedObject类实现本地存储信息,SharedObject类用于在用户计算机上读取和存 储有限的数据量,共享对象提供永久贮存在用户计算机上的对象之间的全局实时数据共享;
本地共享对象是作为一些单独的文件来存储的,它们的文件扩展名 为.SOL。默认时,它们的尺寸为不超过100kB,并且不会过期——这一点与传统的HTTP Cookie不同(4KB);
本地共享对象并不是基于浏览器的,所以普通的用户不容易删除它们。如果要删掉它们的话,首先要知道这些文件所在的具体位 置。这使得本地共享对象能够长时间的保留在本地系统上。
要实现Flash Cookie永远存储的功能,显然,首先要实现Flash Cookie与Http Cookie的互通,所以,在技术上使用JavaScript与ActionScript的来进行沟通显然是最好的选择,因为在这两种语言之间,除了语法 上相近,从沟通上也有着完美的实现。下面我们来看看实现流程(如图所示):
这里给一个实现的 demo,点窗口→动作,我们就可以写actionscript3的代码了,然后文件→发布成 .swf 文件:
//导入ExternalInterface类 import flash.external.ExternalInterface; flash.system.Security.allowDomain("http://localhost"); flash.system.Security.allowDomain("http://127.0.0.1"); //允许任何域都可以访问 flash.system.Security.allowDomain("*"); function setFC(userName:String, sex:String) { var FlashCookie:SharedObject = SharedObject.getLocal("testFlashCookie"); FlashCookie.data.cookie["userName"] = userName; FlashCookie.data.cookie["sex"] = sex; FlashCookie.flush(); } function getFC():String { var FlashCookie:SharedObject = SharedObject.getLocal("testFlashCookie"); return FlashCookie.data.cookie["userName"]; } function setFCUserObj(obj:Object) { var FlashCookie:SharedObject = SharedObject.getLocal("testFlashCookie"); if (FlashCookie.data.cookie == undefined) { //var obj:Object = {}; //obj[key] = value; FlashCookie.data.cookie = obj; } else { for (var key:String in obj) { FlashCookie.data.cookie[key] = obj[key]; } } //FlashCookie.data.userName = obj.userName; //FlashCookie.data.sex = obj.sex; FlashCookie.flush(); } //允许js)调用flash中的getFC(),setFC(),setFCUserObj ExternalInterface.addCallback("getFC", getFC); ExternalInterface.addCallback("setFC", setFC); ExternalInterface.addCallback("setFCUserObj", setFCUserObj);
再用 flask 搭一个简单的页面:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN"> <head> <title>testFC</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <style type="text/css" media="screen"> html, body { height: 100%; background-color: #ffffff; } body { margin: 0; padding: 0; overflow: hidden; } #flashContent { width: 100%; height: 100%; } </style> <script type="application/javascript"> function setCookie(c_name, value, expiredays) { var exdate = new Date() exdate.setDate(exdate.getDate() + expiredays) document.cookie = c_name + "=" + escape(value) + ((expiredays == null) ? "" : ";expires=" + exdate.toGMTString()) } function getCookie(c_name) { if (document.cookie.length > 0) { c_start = document.cookie.indexOf(c_name + "=") if (c_start != -1) { c_start = c_start + c_name.length + 1 c_end = document.cookie.indexOf(";", c_start) if (c_end == -1) c_end = document.cookie.length return unescape(document.cookie.substring(c_start, c_end)) } } return "" } </script> <script type="text/javascript"> //搭建js与flash互通的环境 function thisMovie() { if (navigator.appName.indexOf("Microsoft") != -1) { return window["testFC"]; } else { return document["testFC"]; } } function setFCUseObj() { c_name = getCookie("userName") c_sex = getCookie("sex") if (c_name == "") { alert("当前 jCookie: " + c_name + "/n" + "当前 flash cookie: " + thisMovie().getFC()) var ajaxRequest = new XMLHttpRequest(); ajaxRequest.open("GET", "http://127.0.0.1:5000/add", false); ajaxRequest.send(null); c_name = getCookie("userName") + Math.random(); c_sex = getCookie("sex") + Math.random(); } {# expiredays = 1#} {# setCookie(key, value, expiredays)#} var obj = new Object(); obj.userName = c_name; obj.sex = c_sex; thisMovie().setFCUserObj(obj); } function getFC() { alert(thisMovie().getFC()); } function setFC() { thisMovie().setFC("June_flashCookie", "male"); } </script> </head> <body> <input type="button" onclick="setFC()" value="setFC"/> <input type="button" onclick="getFC()" value="getFC"/> <input type="button" onclick="setFCUseObj()" value="setFCUseObj"/> <div id="flashContent"> <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,19,0" width="1" height="1" id="testFC" title="testFC"> <param name="allowScriptAccess" value="always"/> <param name="movie" value="testFC.swf"> <param name="quality" value="high"> <param name="wmode" value="transparent"/> <embed src="static/testFC.swf" name="testFC" quality="high" allowScriptAccess="always" swLiveConnect="true" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" width="1" height="1"></embed> </object> </div> </body> </html>
最后再配个简单的 cgi:
from flask import Flask, request, Response, make_response, render_template import time app = Flask(__name__) @app.route('/') def hello_world(): return 'hello world' @app.route('/add') def login(): res = Response('add cookies') res.set_cookie(key='userName', value='lisi_jsCookie', expires=time.time() + 10 * 60) res.set_cookie(key='sex', value='unKnown', expires=time.time() + 10 * 60) return res @app.route('/testFC') def cookietest(): return render_template("testFC.html") if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)
优势是显而易见的,缺陷在于:
部署相对 HTTP Cookie 复杂了,存在一定的开发维护成本。
兼容性不够好, 目前只支持特定的平台与特定的浏览器,比如 苹果的全系列产品都不支持 flash,以至于2012年开始 adobe 已经彻底放弃了移动端 flash 的更新,这样 Android 未来也不会存在 flash 这一技术了。
可以跨浏览器使用,只要浏览器调用的是同一个Flash组件(如Internet Explorer和Mozilla Firefox之间信息便可共享,但Google Chrome和Mozilla Firefox便不行)。
虽然可以跨浏览器,但隐身模式无效。可以被很容易地清除。
前面的两种方法都存在一定的缺陷,在复杂多变的用户场景里,数据可能和真实的误差很大,那有没有办法进一步提高 Cookie 追踪用户身份的准确度呢?也有,那就是最后聊到的 EverCookie,其实它也不是什么新的 cookie 技术,只是利用客户端各种存储区域,尽可能的存储多的 cookie 副本,以防某处 cookie 被删除可以恢复,相当于 cookie 也有了类似 hadoop 多副本灾备机制,同时生成 cookieID 的算法参考了更多的客户端标识和软硬件特征,让 cookieID 具有更高的稳定性、唯一性、可辨识性,不随算法本身随机性的影响。
Evercookie不仅仅是难删除,而是会积极“反抗”删除。方法就是在用户电脑里,利用不同的存储机制不断地复制自己,或者在副本丢失或到期作废时让自己重新复活。具体来说,Evercookie在创建cookie时会使用如下存储机制:
标准HTTP cookie
Local Shared Objects (Flash cookie)
Silverlight Isolated Storage
以自动生成、强制缓存的PNG像素图片的RGB值形式保存cookie,使用HTML5 Canvas标签读取像素图片(cookie)
在浏览器历史记录中存储cookie
在HTTP ETag中存储cookie
在浏览器缓存中存储cookie
window.name缓存
Internet Explorer userData
HTML5 Session Storage
HTML5 Local Storage
HTML5 Global Storage
HTML5 Database Storage(SQLite)
开发人员计划增加如下功能:
HTTP Authentication缓存
使用Java基于NIC信息产生唯一键
上面的 EverCookie 看起来气场十足,很完美,其实只是理想太美好,显示依旧很残酷——一样的存在诸多缺陷,只是它把一些缺陷不足尽可能降低了而已。
以 canvas指纹 为例:
取代cookie的网站追踪技术:”帆布指纹识别”初探
虽然解决了 cookie 的稳定性——无法删除,但是唯一性、可辨识性并没有解决——重复率太高、ID容易变化:
canvas指纹验证测试报告
这里有个在线指纹测试的例子:
http://jude.me/fingerprint/
从文初的图上可以看到,在 PC 时代,追踪用户身份技术方案多,也挺靠谱的,但是随着移动互联网大潮的到来,用户逐渐转向了 M 和 APP,形成三大平台三足鼎立的局面,这三大平台的软硬件技术方案各异,比如苹果系列的产品不支持 flash、不允许随便种植 cookie,而 Android 虽然开放,但是开放的尺度太大了,导致了很严重的软硬件碎片化的问题,这给技术方案的通用兼容性带来了严重的问题。应用又分为 NativeAPP 和 webAPP,前者可以很好的和系统结合,拿到系统的硬件信息特征,比如 MAC、IMEI,而 webAPP 大都受限于浏览器隐私策略保护和前端技术限制,没法拿到系统的硬件信息,这就直接导致无法生成一个基于硬件的唯一的、稳定的、准确的“用户ID”,而且想要三端用户身份都打通就成了一个难事,比如:公司三端的用户重合度是 100%,每端 UV 都是一亿,那么三端的总 UV 应该是一亿,但是以现有业界的 cookieID 技术方案来统计 UV,会得出三端总 UV 是三亿的错误结论,而这目前业界也还没有很完善、通用的解决方案。
总结下在移动互联网时代,用户身份识别与追踪的新挑战有两点:
三端用户身份无法打通、统一
追踪识别的成本越来越高,方案越来越复杂化
这或许是商业行为与用户隐私的一场持久博弈,而在这场博弈的背后技术又将会扮演什么角色呢?
[1] Javascript-Flash-Cookies
https://github.com/nfriedly/Javascript-Flash-Cookies
[2] flash cookie的制作和使用例子详解 一
http://ylq365.iteye.com/blog/1873382
[3] 用户数据跟踪之Flash Cookies
http://www.biaodianfu.com/flash-cookies.html
[4] 使用Flash Cookie技术在客户端永久保存HTTP Cookie
http://www.cnblogs.com/dcba1112/archive/2011/05/05/2037715.html
[5] 不用Cookie的“Cookie”技术:etag
http://blog.jobbole.com/46266/
[6] 网站数据收集
https://support.google.com/partners/answer/6083646?hl=zh-Hans
[7] php 如何对客户端 pc 生成唯一标识?
https://www.v2ex.com/t/133910
[8] 防恶意点击代码系统思路与实现
http://wenku.baidu.com/view/6c0b0749be1e650e52ea9917
[9] Evercookie(永远删不掉的cookie)
http://www.ituring.com.cn/article/35102
[9] 如何设置一个永远无法删除的Cookie
http://www.biaodianfu.com/zombie-cookie.html
[9] 关于浏览器身份追踪技术的研究与整理
http://blog.zsxsoft.com/post/11
[10] evercookie
https://github.com/samyk/evercookie
https://github.com/decli/flask-fingerprint
[11] 取代cookie的网站追踪技术:”帆布指纹识别”初探
http://security.tencent.com/index.php/blog/msg/59
[12] canvas指纹验证测试报告
http://blog.csdn.net/huangm_fat/article/details/38522939
[13] 在线指纹测试例子:
http://jude.me/fingerprint/