平常生活中,生物識別技術已是多數智能手機的標配,大多數手機具有人臉識別、指紋識別等功能,目前的指紋識別技術已經很是成熟。但咱們今天要聊的並非生物識別技術中的指紋識別,而是瀏覽器指紋。不少人對這項技術是又愛又恨,這到底是爲何呢?那咱們今天就來深刻了解下瀏覽器指紋。html
瀏覽器指紋能夠經過瀏覽器對網站可見的配置、設置信息,來跟蹤 Web 瀏覽器,它就像咱們人手上的指紋同樣,具備個體辨識度,只不過現階段瀏覽器指紋辨別的是瀏覽器。web
瀏覽器指紋辨識的信息能夠是 UA、時區、地理位置或者是使用的語言等等,瀏覽器所開發的信息決定了瀏覽器指紋的準確性。算法
對於網站而言,拿到瀏覽器指紋並無實際價值,真正有價值的是瀏覽器指紋對應的用戶信息。做爲網站站長,收集用戶瀏覽器指紋並記錄用戶的操做,是一個有價值的行爲,特別是針對沒有用戶身份的場景。canvas
例如一個視頻網站,未註冊該網站的用戶 A 喜歡瀏覽二次元的視頻,經過瀏覽器指紋記錄這個,那麼下次能夠直接向該瀏覽器推送二次元的視頻。由於如今的上網設備大都是私人的,這樣的推送方式很容易得到大部分用戶的好感,從而使之成爲網站的用戶。數組
瀏覽器指紋的發展瀏覽器
瀏覽器指紋技術的發展跟大多數技術同樣,並不是一蹴而就的,現有的幾代瀏覽器指紋技術是這樣的:cookie
目前瀏覽器指紋的追蹤技術能夠算是進入 2.5 代,這麼說是由於跨瀏覽器識別指紋的問題仍沒有解決。app
信息熵(entropy)是接收的每條消息中包含的信息的平均量,信息熵越高,則能傳輸越多的信息,信息熵越低,則意味着傳輸的信息越少。框架
瀏覽器指紋是由許多瀏覽器的特徵信息綜合起來的,其中特徵值的信息熵也不盡相同。所以,指紋也分爲基本指紋和高級指紋。dom
基本指紋
基本指紋就是容易被發現和修改的部分,如 http 的 header。
{ "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Host": "httpbin.org", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36" }}
除了 http 中拿到的指紋,還能夠經過其餘方式來得到瀏覽器的特徵信息,例如:
拿到這些值後能夠進行一些運算,獲得瀏覽器指紋具體的信息熵以及瀏覽器的 uuid。
這些信息就相似人類的體重、身高、膚色同樣,有很大的重複機率,只能做爲輔助識別,因此咱們須要更精確的指紋來判斷惟一性。
高級指紋
普通指紋是不夠區分獨特的我的,這時就須要高級指紋,將範圍進一步縮小,甚至生成一個獨一無二的跨瀏覽器身份。
用於生產指紋的各個信息,有權重大小之分,信息熵大的將擁有較大的權重。
在論文《Cross-Browser Fingerprinting via OS and Hardware Level Features [http://yinzhicao.org/TrackingFree/crossbrowsertracking_NDSS17.pdf]》中更是詳細研究了各個指標的信息熵和穩定性。
從該論文中能夠看出,時區、屏幕分辨率和色深、Canvas、webGL 的信息熵在跨瀏覽器指紋上的權重是比較大的。下面咱們就來看看這些高級指紋都包含了些什麼信息。
Canvas 指紋
Canvas 是 HTML5 中的動態繪圖標籤,也能夠用它生成圖片或者處理圖片。即使使用 Canvas 繪製相同的元素,可是因爲系統的差異,字體渲染引擎不一樣,對抗鋸齒、次像素渲染等算法也不一樣,Canvas 將一樣的文字轉成圖片,獲得的結果也是不一樣的。
實現代碼大體爲:在畫布上渲染一些文字,再用 toDataURL 轉換出來,即使開啓了隱私模式同樣能夠拿到相同的值。
function getCanvasFingerprint () { var canvas = document.createElement('canvas'); var context = canvas.getContext("2d"); context.font = "18pt Arial"; context.textBaseline = "top"; context.fillText("Hello, user.", 2, 2); return canvas.toDataURL("image/jpeg"); } getCanvasFingerprint()
流程很簡單,渲染文字,toDataURL 是將整個 Canvas 的內容導出,獲得值。
WebGL 指紋
WebGL(Web圖形庫)是一個 JavaScript API,可在任何兼容的 Web 瀏覽器中渲染高性能的交互式 3D 和 2D 圖形,而無需使用插件。WebGL 經過引入一個與 OpenGL ES 2.0 很是一致的 API 來作到這一點,該 API 能夠在 HTML5 元素中使用。這種一致性使 API 能夠利用用戶設備提供的硬件圖形加速。網站能夠利用 WebGL 來識別設備指紋,通常能夠用兩種方式來作到指紋生產:
WebGL 報告——完整的 WebGL 瀏覽器報告表是可獲取、可被檢測的。在一些狀況下,它會被轉換成爲哈希值以便更快地進行分析。
WebGL 圖像 ——渲染和轉換爲哈希值的隱藏 3D 圖像。因爲最終結果取決於進行計算的硬件設備,所以此方法會爲設備及其驅動程序的不一樣組合生成惟一值。這種方式爲不一樣的設備組合和驅動程序生成了惟一值。
能夠經過 Browserleaks test 檢測網站來查看網站能夠經過該 API 獲取哪些信息。
產生WebGL指紋原理是首先須要用着色器(shaders)繪製一個梯度對象,並將這個圖片轉換爲Base64字符串。而後枚舉WebGL全部的拓展和功能,並將他們添加到Base64字符串上,從而產生一個巨大的字符串,這個字符串在每臺設備上多是很是獨特的。
例如fingerprint2js庫的 WebGL 指紋生產方式:
// 部分代碼 gl = getWebglCanvas() if (!gl) { return null } var result = [] var vShaderTemplate = 'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}' var fShaderTemplate = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}' var vertexPosBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer) var vertices = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.732134444, 0]) // 建立並初始化了Buffer對象的數據存儲區。 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW) vertexPosBuffer.itemSize = 3 vertexPosBuffer.numItems = 3 // 建立和初始化一個WebGLProgram對象。 var program = gl.createProgram() // 建立着色器對象 var vshader = gl.createShader(gl.VERTEX_SHADER) // 下兩行配置着色器 gl.shaderSource(vshader, vShaderTemplate) // 設置着色器代碼 gl.compileShader(vshader) // 編譯一個着色器,以便被WebGLProgram對象所使用 var fshader = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(fshader, fShaderTemplate) gl.compileShader(fshader) // 添加預先定義好的頂點着色器和片斷着色器 gl.attachShader(program, vshader) gl.attachShader(program, fshader) // 連接WebGLProgram對象 gl.linkProgram(program) // 定義好的WebGLProgram對象添加到當前的渲染狀態 gl.useProgram(program) program.vertexPosAttrib = gl.getAttribLocation(program, 'attrVertex') program.offsetUniform = gl.getUniformLocation(program, 'uniformOffset') gl.enableVertexAttribArray(program.vertexPosArray) gl.vertexAttribPointer(program.vertexPosAttrib, vertexPosBuffer.itemSize, gl.FLOAT, !1, 0, 0) gl.uniform2f(program.offsetUniform, 1, 1) // 從向量數組中繪製圖元 gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexPosBuffer.numItems) try { result.push(gl.canvas.toDataURL()) } catch (e) { /* .toDataURL may be absent or broken (blocked by extension) */ }
文章開頭也提到了,不少人對瀏覽器這項技術是又愛又恨。由於一大堆網站使用各類技術來「生成」用戶指紋,以便給網站用戶帶來更精準的推薦和符合用戶的瀏覽習慣。而用戶在享受技術帶來便利的同時,也難免會有「隱私泄露」的焦躁和不安感。那麼咱們如何防止被生成「用戶指紋」呢?
混淆 Canvas 指紋
咱們已經瞭解了是如何獲取 canvas 指紋的,那麼應該如何防範被惡意獲取呢?想混淆 Canvas 指紋,只須要在 toDataURL 獲得的結果上作手腳就能夠。
toDataURL() 將整個canvas的內容導出,咱們須要將 Canvas 中的部份內容修改,這個時候能夠經過 getImageData() 複製畫布上指定矩形的像素數據,而後經過 putImageData()將圖像數據放回,而後再使用 toDataURL() 導出的圖片就有了差別。
CanvasRenderingContext2D.getImageData() 返回一個ImageData對象,用來描述 Canvas 區域隱含的像素數據。這個區域經過矩形表示,起始點爲(sx, sy)、寬爲sw、高爲sh。
ImageData 接口描述了<Canvas>元素的一個隱含像素數據的區域,能夠由 ImageData() 方法構造,或者由canvas 在一塊兒的 CanvasRenderingContext2D 對象的建立方法:createImageData() 和 getImageData()。
ImageData 對象存儲着canvas對象真實的像素數據,它包含幾個只讀屬性:
Uint8ClampedArray 類型的一位數組,包含着 RGBA 的整型數據,範圍在 0~255。它能夠視做初始像素數據,每一個像素用 4 個 1 bytes 值(按照 red、green、blue、alpha 的順序),每一個顏色值用0~255 中的數字表明。每一個部分被分配到一個數組內的連續索引,左上角第一個像素的紅色部分,位於數組索引的第 0 位。像素從左到右從上到下被處理,遍歷整個數組。
Unit8ClampedArray 包含 高度寬度4 bytes數據,索引值從 0 ~ (wh4)-1 。
例如,讀取圖片中位於第 50 行,200 列的像素的藍色部分,則:
const blueComponent = imageData[50*(imageData.width * 4) + 200*4 + 2]
下面是實現混淆 Canvas 指紋的方法:
const toBlob = HTMLCanvasElement.prototype.toBlob; const toDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.manipulate = function() { const {width, height} = this; // 拿到在進行toDataURL或者toBlob前的canvas所生成的CanvasRenderingContext2D const context = this.getContext('2d'); const shift = { 'r': Math.floor(Math.random() * 10) - 5, 'g': Math.floor(Math.random() * 10) - 5, 'b': Math.floor(Math.random() * 10) - 5 }; const matt = context.getImageData(0, 0, width, height); // 對getImageData生成的imageData(像素源數據)中的每個像素的r、g、b部分的值進行進行隨機改變從而生成惟一的圖像。 for (let i = 0; i < height; i += Math.max(1, parseInt(height / 10))) { for (let j = 0; j < width; j += Math.max(1, parseInt(width / 10))) { const n = ((i * (width * 4)) + (j * 4)); matt.data[n + 0] = matt.data[n + 0] + shift.r; // 加上隨機擾動 matt.data[n + 1] = matt.data[n + 1] + shift.g; matt.data[n + 2] = matt.data[n + 2] + shift.b; } } context.putImageData(matt, 0, 0); // 從新放回去 // 修改prototype.toBlob Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { value: function() { if (script.dataset.active === 'true') { try { this.manipulate(); // 在每次toBlob前,先混淆下ImageData } catch(e) { console.warn('manipulation failed', e); } } return toBlob.apply(this, arguments); } }); // 修改prototype. toDataURL Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', { value: function() { if (script.dataset.active === 'true') { try { this.manipulate(); // 在每次toDataURL前,先混淆下ImageData } catch(e) { console.warn('manipulation failed', e); } } return toDataURL.apply(this, arguments); } });
混淆其餘指紋
與前面混淆canvas指紋混淆的思路是一致的,都是更改被獲取對象的原型的方法。
好比混淆時區,就是更改 Date.prototype.getTimezoneOffset 的返回值。
混淆分辨率則是更改documentElement.clientHeight documentElement.clientWidth
混淆 WebGL 則要更改 WebGLbufferData getParameter方法等等。
固然,咱們也有一些簡單的方法來防止被生成用戶指紋。例如咱們能夠經過瀏覽器的擴展插件(Canvas Blocker、WebGL Fingerprint Defender、Fingerprint Spoofing等),在網頁加載前執行一段 JS 代碼,更改、重寫 JS 的各個函數來阻止網站獲取各類信息,或返回一個假的數據,以此來保護咱們的隱私信息。