現在消費者已越來越能接受線上付費觀看影音服務,例如官方提供的職業運動直播(NBA、MLB 等),以及各種追劇平台(Netflix、Line TV、愛奇藝等)。而消費者總是希望花錢的效益最大化,如果買一個帳號能在三台裝置收看,則可能找三個人合資分攤費用。 站在影音平台的立場,技術上必須防堵消費者同時登入超過 3 個裝置,因此得辨識出個別使用者上網的機器之硬體差異性。那麼這件事要如何做到呢?判斷單一裝置的身份認證,最簡單的方式為獲取「MAC address」,也可看做是該裝置的網卡實體位址,這絕對是獨一無二可供辨識的 ID 字串。 非網頁的環境下取得「MAC 位址」不難,然而線上影音平台架設在網頁,前端使用 Javascript 並沒有權限可取得「MAC 位址」,否則會有安全性問題。那麼網頁環境是否還有辦法能用 JS 追蹤訪客裝置?不然的話消費者買一個帳號卻得以從 100 個裝置登入,這些影音服務就得關門了! 最近有研究網頁裝置辨識的需求,本篇就來整理相關心得,讓每個訪客的裝置都能產生獨有的指紋辨識記錄,並介紹實用的 JS 瀏覽器指紋工具。
一、產生瀏覽器指紋的原理
由於網頁環境不易取得實體裝置資訊,退而求其次產生瀏覽器指紋較為可行。消費者一次只會從裝置使用一個瀏覽器上影音平台,所以辨識瀏覽器指紋已經足夠。 如何辨識使用者的瀏覽器特徵,大致整理出下面這些作法: 1. Google、FB 建立的使用者 ID其實每個人上網的一舉一動已被 Google、FB 監控,這些大公司早就研發出辨識使用者的技術,在每個人的電腦中埋下記錄。既然輪子已經造好,最簡單的作法是直接拿來用而不是自己研發,只要找出 Google、FB 留下的痕跡即可。 參考這個討論串「How do I uniquely identify computers visiting my web site?」,原 PO 詢問如何辨識他網站的訪客裝置,有人回答 Google Analytics(簡稱GA) 會使用 cookie 留下每個裝置的 ID 字串,讀取這個 ID 字串會是最方便的捷徑。 那麼要如何讀取 GA 留下的 ID 字串呢?這個討論串「how to get the Google analytics client ID」說明了如何讀取 GA 提供的「Client ID」(訪客 ID):ga.getAll()[0].get('clientId');
使用 JS 執行以上語法就行了,非常簡單吧!同樣的原理,網站如果有安裝 FB pixel 相信也能取得類似的 ID。 只不過這方法雖然簡單,但 cookie 畢竟可能會被清除、修改,比較難讓人安心。2. Canvas 畫布指紋最早被利用來製作瀏覽器指紋的技術是 HTML5 的 Canvas 畫布功能,詳細原理可參考這篇「解讀瀏覽器指紋」→「canvas指紋」。 其原理簡單說就是,因每個裝置的軟硬體設備不同,那麼經由 canvas 技術繪製出來的同一幅圖像都會有肉眼看不出的微小差別,而經由演算法就能產生該圖像的一組 ID 字串,可做為瀏覽器指紋之用。 如果不了解這樣的說明,可以閱讀原文的幾張附圖,就能瞭解微小像素在不同裝置會有什麼樣的差異。 3. 綜合指紋 Canvas 指紋已經足以分辨出大部分的使用者裝置,但演算法產生的 ID 字串難免會遇到重複的情形,此時再組合該裝置的其他軟硬體資訊,只要加入的資訊越多,遇到重複的機率就越低。 而其他可以做為指紋的資訊可參考「瀏覽器指紋追蹤技術簡述」: - 基本資訊:例如瀏覽器版本、作業系統、語系、時區、螢幕解析度、字體....等等
- Audio 指紋:類似 Canvas 的作法,利用軟硬體差異,將一段音訊經演算法產生 ID 指紋
- WebRTC:利用較新瀏覽器版本才支援的功能,來取得經通信技術才能獲得的資訊。
二、FingerprintJS V3
1. 工具介紹免費開源軟體中功能最強的要屬 FingerprintJS:- 官網:Github
- DEMO 頁面:fingerprintjs
<script> function initFingerprintJS() { FingerprintJS.load().then(fp => { // The FingerprintJS agent is ready. // Get a visitor identifier when you'd like to. fp.get().then(result => { // This is the visitor identifier: const visitorId = result.visitorId; console.log(visitorId); }); }); } </script> <script async src="//cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@3/dist/fp.min.js" onload="initFingerprintJS()"></script>
2. 指紋含括資訊上圖為 DEMO 頁面提供的資訊,顯示了 FingerprintJS V3 計算瀏覽器指紋 ID 時,所包含的所有資訊,例如瀏覽器版本、語系、記憶體、解析度...等等,截圖只是一部份資訊,螢幕往下捲可看到一共有將近 30 項資訊。 3. 優缺點此工具的優點為:版本最新,會包含較新的辨識技術,不同裝置的指紋 ID 要重複較困難。 不過從我的角度來看無法做為首選,因為有以下缺點: - 新版本沒有 API 使用說明,也就是阻止了前端工程師的開發工作,只能取得指紋 ID,無法進行其他操作
- 新版本程式碼經過壓縮不易辨識,使用彈性極差,原因在於要另外推廣其付費版本(官網推薦使用者升級為 PRO 版),付費版本可前往「FingerprintJS」
- 新版本一律強制偵測、檢驗所有近 30 種項目,所以 JS 執行時間會比較長一些
三、FingerprintJS V2
1. 工具介紹還好 FingerprintJS 的作者還保留了舊版 V2 的頁面:- 官網:Github
<script src='//cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@2/dist/fingerprint2.min.js'> </script> <script> if (window.requestIdleCallback) { requestIdleCallback(function() { Fingerprint2.get(function(components) { console.log(components) // an array of components: {key: ..., value: ...} }) }) } else { setTimeout(function() { Fingerprint2.get(function(components) { console.log(components) // an array of components: {key: ..., value: ...} }) }, 500) } </script>
使用瀏覽器開發人員工具可看到檢測項目: - 上圖可與 V3 比對一下,看檢測項目有哪些差異
- 標示「not available」的項目可考慮不用檢測,因為可能在其他使用者的裝置也是測不出來
- 「canvas」與「webgl」兩者可考慮選一個就好,都是對繪圖能力做檢驗,但「webgl」含括的資訊量更多
Fingerprint2.get(options, function (components) { var values = components.map(function (component) { return component.value }) var murmur = Fingerprint2.x64hash128(values.join(''), 31) })
murmur 的數值即為「瀏覽器指紋 ID」。 4. 操作補充說明 V2 可對所有檢測項目進行設定,也可排除要檢測的項目,操作說明可在官網文件捲到「Options」,官方提供的範例如下: var options = {fonts: {extendedJsFonts: true}, excludes: {userAgent: true}}
此範例可對「fonts」進行設定,也可排除「userAgent」這一項,細節需參照官方原始碼第 245 行「defaultOptions 」 設定完 options 後就能執行工具,可參考前面「3. 取得指紋 ID」的範例程式碼,該處程式碼即為帶入 options 的結果。 四、canvas 指紋
以前端開發工程師而言,使用「三、FingerprintJS V2」算是最佳選擇。 不過這裡提供另外一個選項,如果覺得研究 V2 的說明書太麻煩,有國外網友從 FingerprintJS 擷取幾個重要項目來使用,檢驗項目算是以 Canvas 為主,發佈在 Github:所有檢測的項目作者已列在官網,程式碼較短小也節省執行時間,我算是認同此作法。 以下提供此版本整理過的程式碼,直接執行就能取得瀏覽器指紋 ID:var Fingerprint=function(a){var b,c;b=Array.prototype.forEach;c=Array.prototype.map;this.each=function(j,h,g){if(j===null){return}if(b&&j.forEach===b){j.forEach(h,g)}else{if(j.length===+j.length){for(var f=0,d=j.length;f<d;f++){if(h.call(g,j[f],f,j)==={}){return}}}else{for(var e in j){if(j.hasOwnProperty(e)){if(h.call(g,j[e],e,j)==={}){return}}}}}};this.map=function(g,f,e){var d=[];if(g==null){return d}if(c&&g.map===c){return g.map(f,e)}this.each(g,function(j,h,i){d[d.length]=f.call(e,j,h,i)});return d};if(typeof a=="object"){this.hasher=a.hasher;this.canvas=a.canvas}else{if(typeof a=="function"){this.hasher=a}}};Fingerprint.prototype={get:function(){var a=[];a.push(navigator.userAgent);a.push(navigator.language);a.push(screen.colorDepth);a.push(this.getScreenResolution().join("x"));a.push(new Date().getTimezoneOffset());a.push(this.hasSessionStorage());a.push(this.hasLocalStorage());a.push(this.hasIndexDb());if(document.body){a.push(typeof(document.body.addBehavior))}else{a.push(typeof undefined)}a.push(typeof(window.openDatabase));a.push(navigator.cpuClass);a.push(navigator.platform);a.push(navigator.doNotTrack);a.push(this.getPluginsString());if(this.canvas&&this.isCanvasSupported()){a.push(this.getCanvasFingerprint())}if(this.hasher){return this.hasher(a.join("###"),31)}else{return murmurhash3_32_gc(a.join("###"),31)}},hasLocalStorage:function(){try{return !!window.localStorage}catch(a){return true}},hasSessionStorage:function(){try{return !!window.sessionStorage}catch(a){return true}},hasIndexDb:function(){try{return !!window.indexedDB}catch(a){return true}},isCanvasSupported:function(){var a=document.createElement("canvas");return !!(a.getContext&&a.getContext("2d"))},isIE:function(){if(navigator.appName==="Microsoft Internet Explorer"){return true}else{if(navigator.appName==="Netscape"&&/Trident/.test(navigator.userAgent)){return true}}return false},getPluginsString:function(){if(this.isIE()){return this.getIEPluginsString()}else{return this.getRegularPluginsString()}},getRegularPluginsString:function(){return this.map(navigator.plugins,function(b){var a=this.map(b,function(c){return[c.type,c.suffixes].join("~")}).join(",");return[b.name,b.description,a].join("::")},this).join(";")},getIEPluginsString:function(){if(window.ActiveXObject){var a=["ShockwaveFlash.ShockwaveFlash","AcroPDF.PDF","PDF.PdfCtrl","QuickTime.QuickTime","rmocx.RealPlayer G2 Control","rmocx.RealPlayer G2 Control.1","RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)","RealVideo.RealVideo(tm) ActiveX Control (32-bit)","RealPlayer","SWCtl.SWCtl","WMPlayer.OCX","AgControl.AgControl","Skype.Detection"];return this.map(a,function(b){try{new ActiveXObject(b);return b}catch(c){return null}}).join(";")}else{return""}},getScreenResolution:function(){return(screen.height>screen.width)?[screen.height,screen.width]:[screen.width,screen.height]},getCanvasFingerprint:function(){var c=document.createElement("canvas");var b=c.getContext("2d");var a="CANVAS_FINGERPRINT";b.textBaseline="top";b.font="14px 'Arial'";b.textBaseline="alphabetic";b.fillStyle="#f60";b.fillRect(125,1,62,20);b.fillStyle="#069";b.fillText(a,2,15);b.fillStyle="rgba(102, 204, 0, 0.7)";b.fillText(a,4,17);return c.toDataURL()}}; var javaHashCode = function(string, K) {var hash = 0;if (string.length === 0) {return hash;}for (var i = 0; i < string.length; i++) {char = string.charCodeAt(i);hash = K*((hash<<5)-hash)+char;hash = hash & hash;}return hash;}; var fingerprint = new Fingerprint({hasher: javaHashCode}); console.log(fingerprint.get())
更多 Javascript 使用技巧: