最近,我在使用javascript开发一个基于Chrome的插件,遇到了一个有意思的需求。插件需要生成一个授权码(code),但为了确保安全性,这个code必须与设备绑定,防止被不同的设备使用,限制一个code只能在一个设备上使用。这个需求带来了一个问题:我该如何在前端中获取当前设备的唯一标识呢?
在对浏览器的限制做了进一步了解,因为涉及到用户隐私问题,因为MAC地址是一种物理地址,能够作为设备的唯一标识,如果被网站轻易获取,可能会导致用户隐私泄露和安全风险增加。所以现在浏览器不允许前端JavaScript直接获取设备的MAC地址或其它能够明确标识设备硬件的敏感信息。那么通过前端直接获取设备的唯一硬件标识符是不可能的。至此全剧终,不用往下看了🤣
哈哈哈,虽然不能直接获取到设备唯一信息,但是需求还要做,毕竟解决不了提需求的人啊。在这里整理了几种思路:
可以通过后端服务器记录IP地址、User-Agent等信息来间接识别用户
通过收集浏览器的各种配置和环境信息(如屏幕分辨率、字体、插件列表等),生成一个相对唯一的标识符
在用户首次安装插件时生成一个UUID并存储在Cookie或localStorage中,以此作为该设备的唯一标识。但需注意,用户清除浏览器数据时,此ID存在丢失的风险。
先给大家分析一下这种方法的优缺点 :
这个方法并不能精确地区分内网中的每个设备,假设现在有10个设备位于同一内网,并通过相同的外网出口IP地址访问互联网时,仅凭后端服务器记录的IP地址无法区分这10个设备,因为它们对外显示的是同一个公网IP地址。
User-Agent,它是浏览器或客户端应用程序发送请求时携带的一个字符串头,包含有关浏览器类型、版本、操作系统等信息。虽然User-Agent可以提供关于用户使用的浏览器和系统的信息,但对于来自同一内网的不同设备,如果这是批量采购的设备,在设备、浏览器和操作系统没有其他差异,它们可能会是相同的User-Agent字符串。
另外就是我们的后台服务只是提供简单的授权码验证服务,增加这部分逻辑显得有些多余。
可以生成一个随机的 UUID,作为设备的唯一标识符,这个方法完全可以满足需求。
示例代码:
// 生成UUID function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } UUID的版本4使用随机数生成,重复的概率极低。然而,如果你担心在非常大的数据集中可能会有重复的情况发生,或者你想要确保在分布式系统中生成的UUID在时间上有一定的顺序,那么将UUID与时间戳结合起来。
// 时间戳可以提供一个大致的创建时间顺序 // UUID可以确保在同一个时间戳内生成的标识符的唯一性 function generateTimestampedUUID() { // 获取当前时间的时间戳(毫秒) const timestamp = Date.now(); // 生成UUID v4 const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); // 将时间戳和UUID拼接起来 return `${timestamp}-${uuid}`; } console.log(generateTimestampedUUID()); 简单的来说就是,通过JavaScript获取多种客户端信息,这些信息可以用来生成一个相对唯一的标识符,这也是我们常说的“浏览器指纹”。
那么我们能获取到客户端哪些信息呢?
// 屏幕尺寸:屏幕的宽度和高度。 const screenSize = `${screen.width}x${screen.height}`; // 可视区域尺寸:浏览器窗口的宽度和高度。 const viewportSize = `${window.innerWidth}x${window.innerHeight}`; // 颜色深度:屏幕颜色深度。 const colorDepth = screen.colorDepth; // User-Agent:包含浏览器、操作系统及其版本信息。 const userAgent = navigator.userAgent; // 平台:浏览器运行的系统平台。 const platform = navigator.platform; // 语言:浏览器的语言设置。 const language = navigator.language; // 插件列表:浏览器中安装的插件列表 const plugins = Array.from(navigator.plugins).map(plugin => plugin.name).join(','); // Do Not Track:用户是否启用了“请勿跟踪”设置 const doNotTrack = navigator.doNotTrack; // 时区:用户的时区信息 const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; // 时区偏移:与UTC的时区偏移,以分钟为单位。 const timeZoneOffset = new Date().getTimezoneOffset(); // 检测用户系统中可用的字体 const fontList = [ 'Arial', 'Verdana', 'Times New Roman', 'Courier New', 'Georgia', 'Comic Sans MS', 'Trebuchet MS', 'Arial Black', 'Impact' ]; const availableFonts = []; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const text = 'abcdefghijklmnopqrstuvwxyz0123456789'; fontList.forEach((font) => { context.font = `16px ${font}`; const width = context.measureText(text).width; context.font = `16px monospace`; if (context.measureText(text).width !== width) { availableFonts.push(font); } }); const fonts = availableFonts.join(','); // CPU线程数:设备的逻辑处理器数量。 const hardwareConcurrency = navigator.hardwareConcurrency; // 设备内存:设备内存信息,单位为GB。 const deviceMemory = navigator.deviceMemory; // WebGL渲染器:WebGL渲染器信息 function getWebGLRenderer() { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (gl) { const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (debugInfo) { return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); } } return null; } const webGLRenderer = getWebGLRenderer(); // Canvas指纹:利用Canvas API绘制图像并获取其数据 function getCanvasFingerprint() { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); context.textBaseline = 'top'; context.font = '14px Arial'; context.textBaseline = 'alphabetic'; context.fillStyle = '#f60'; context.fillRect(125, 1, 62, 20); context.fillStyle = '#069'; context.fillText('Hello, world!', 2, 15); context.fillStyle = 'rgba(102, 204, 0, 0.7)'; context.fillText('Hello, world!', 4, 17); return canvas.toDataURL(); } const canvasFingerprint = getCanvasFingerprint(); // 设备是否支持触控 const touchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0; 最后做一个综合代码示例,将上述信息组合成一个指纹字符串并进行哈希处理,以生成一个唯一标识符:
function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; // Convert to 32bit integer } return hash.toString(16); } function getScreenSize() { return `${screen.width}x${screen.height}`; } function getViewportSize() { return `${window.innerWidth}x${window.innerHeight}`; } function getFonts() { const fontList = [ 'Arial', 'Verdana', 'Times New Roman', 'Courier New', 'Georgia', 'Comic Sans MS', 'Trebuchet MS', 'Arial Black', 'Impact' ]; const availableFonts = []; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const text = 'abcdefghijklmnopqrstuvwxyz0123456789'; fontList.forEach((font) => { context.font = `16px ${font}`; const width = context.measureText(text).width; context.font = `16px monospace`; if (context.measureText(text).width !== width) { availableFonts.push(font); } }); return availableFonts.join(','); } function getWebGLRenderer() { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (gl) { const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (debugInfo) { return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); } } return null; } function getCanvasFingerprint() { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); context.textBaseline = 'top'; context.font = '14px Arial'; context.textBaseline = 'alphabetic'; context.fillStyle = '#f60'; context.fillRect(125, 1, 62, 20); context.fillStyle = '#069'; context.fillText('Hello, world!', 2, 15); context.fillStyle = 'rgba(102, 204, 0, 0.7)'; context.fillText('Hello, world!', 4, 17); return canvas.toDataURL(); } function generateFingerprint() { const screenSize = getScreenSize(); const viewportSize = getViewportSize(); const colorDepth = screen.colorDepth; const userAgent = navigator.userAgent; const platform = navigator.platform; const language = navigator.language; const plugins = Array.from(navigator.plugins).map(plugin => plugin.name).join(','); const doNotTrack = navigator.doNotTrack; const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timeZoneOffset = new Date().getTimezoneOffset(); const fonts = getFonts(); const hardwareConcurrency = navigator.hardwareConcurrency; const deviceMemory = navigator.deviceMemory; const webGLRenderer = getWebGLRenderer(); const canvasFingerprint = getCanvasFingerprint(); const touchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const fingerprint = [ screenSize, viewportSize, colorDepth, userAgent, platform, language, plugins, doNotTrack, timeZone, timeZoneOffset, fonts, hardwareConcurrency, deviceMemory, webGLRenderer, canvasFingerprint, touchSupport ].join('|'); return hashString(fingerprint); } const fingerprint = generateFingerprint(); const deviceUUID = generateUUID(); console.log('Fingerprint:', fingerprint); console.log('Device UUID:', deviceUUID); 简单的用户追踪:适用于需要记录访问日志或统计用户活动的简单应用。没有严格的设备绑定要求:适用于不需要严格绑定设备的情况,如某些统计和分析用途。长期设备识别:适用于需要在长时间内稳定识别同一设备的场景,如个性化设置保存、长期会话管理等。用户行为追踪:适用于需要追踪用户行为和偏好的应用,如个性化推荐、广告投放等。增强的安全性:适用于需要更高安全性和防欺骗能力的场景,如防止账号共享、在线考试等。无用户登录的情况下识别设备:适用于不需要用户登录即可识别和区分不同设备的应用。