ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript为何无法读取硬件信息?浏览器安全边界全解析

JavaScript为何无法读取硬件信息?浏览器安全边界全解析 我们经常在开发者群或者学习群看到有人问“为什么JavaScript读不了用户电脑的CPU序列号”“为什么我不能用JS直接拿硬盘信息”这类问题几乎每个前端都想过。今天我用项目实战的角度把这个问题彻底拆透顺便把到底哪些硬件相关信息是能拿的、哪些是绝对不能拿的、以及如果你真有强需求该绕哪条路全部讲清楚。你会看到浏览器沙箱、Web安全模型、指纹追踪、再到WebUSB这类新API逐步放开的能力边界这些东西搞懂了以后遇到类似问题不用再猜。先说结论JavaScript不是“没本事”访问硬件信息而是浏览器的安全模型故意不让它访问。这个“故意”是整个Web生态能跑二十多年不出大乱子的根基。1. 先说清楚JavaScript到底跑在一个什么样的环境里1.1 浏览器沙箱不是权限不够而是“故意隔离”很多人不理解沙箱这个词我打个比方。你住在一栋公寓里你可以在自己房间里装修、放家具、摆电器但你不能随便去邻居家拆墙更不能把整栋楼的承重墙砸了。浏览器就是公寓的管理方JavaScript是住在房间里的住户硬件是整栋楼的水电燃气总闸。管理方为了保证所有住户的安全给你一张权限清单你可以开自己房间的灯操作DOM、可以打电话给前台发网络请求、可以看自己房间的门牌号读取location但水电总闸、燃气总管道必须由管理方统一操作。这就是沙箱——一个永远在受限空间内运行的执行环境。JavaScript运行在渲染进程里而这个进程本身就是被操作系统以普通用户权限隔离的。就算网页写得再巧妙绕过JS引擎的层层校验它还得先突破浏览器进程与操作系统之间的边界而这个边界上有内核级的隔离机制。所以很多“为什么不能访问硬件”的答案不在语言层面在架构层面。1.2 从V8引擎到渲染进程JS和系统之间的“墙”有多厚我们来梳理一下JavaScript从源码到访问硬件到底隔了几层。第一层是JS引擎以Chrome的V8为例。V8负责把JavaScript编译成机器码并执行。注意V8本身不提供任何文件系统、硬件访问能力它只提供语言标准里规定的东西比如Object、Array、Promise、Math。你写Math.random()能拿到随机数但V8不会告诉你这个随机数来自哪个物理设备。第二层是Web API层。document.getElementById()、fetch()、localStorage这类API是浏览器暴露给JavaScript的。浏览器在实现这些API时才真正去调用操作系统功能比如fetch底层走的是网络栈canvas底层走的是GPU或CPU绘图。但在这一层浏览器做了一个非常关键的动作——API返回的结果经过裁剪和抽象只给你“够用”的信息。第三层是浏览器进程本身。Chrome的架构是“多进程架构”页面JS跑在renderer进程真正跟操作系统打交道的是browser进程。renderer进程和browser进程之间通过IPC通信而且通信内容是受限的。这意味着即使JS代码在renderer进程里跑了恶意逻辑它也只能请求browser进程做事而browser进程会先检查这次请求是否符合安全策略。这三层墙一叠加结果就是JavaScript能碰到最远的硬件边界就是浏览器进程愿意给你返回的那点信息。2. 为什么不能访问硬件信息安全模型的四个底层原因2.1 恶意脚本的风险放大效应我们设一个场景假设JavaScript能直接读硬件信息那任意网站只要在页面里藏一行脚本就能读取你电脑的CPU序列号、主板编号、MAC地址。这些信息是可唯一标识一台设备的。世界上有大量网站你只访问过一次可能是一个营销活动页一个别人发给你的链接甚至是一张图片背后的CDN域名下的跳转页面。只要它想就能在几百毫秒内扫一圈你的硬件清单。你不给它任何授权它就能给你的设备建立一份长期指纹档案。更麻烦的不是读取而是写入。既然能读硬件万一浏览器某个API暴露了写能力恶意脚本就能让你的CPU风扇转速失控、显卡超频烧毁元器件、固件被篡改。这不是危言耸听历史上针对硬件的恶意代码绝大多数都是利用了操作系统层面的漏洞浏览器如果自己开这个口子等于主动降低攻击门槛。2.2 跨站信息窃取的链条单看“读取硬件信息”这个动作本身好像没什么杀伤力。但如果把硬件信息跟其他数据放到一起问题就严重了。你在购物网站登录了自己的账号浏览器里保存了你的登录态Cookie。现在你打开一个恶意网站它如果能读取硬件信息再配合XSS漏洞或者一个没过滤好参数的搜索框就可能把这台机器的唯一标识和你在购物网站的身份对应起来。以后这个恶意网站就能长期追踪你的上网行为——通过硬件标识识别到你“又回来了”不管你怎么清理Cookie都拦不住。Web安全模型有一条核心原则叫“同源策略”它的目的是隔离不同来源的数据。硬件信息是一种跨源数据如果JavaScript能轻易拿到等同于是给跨源追踪架了一座桥。浏览器厂商花了几十年时间堵CSRF、堵XSS、堵点击劫持不会蠢到在硬件这里留一个后门。2.3 隐私与指纹追踪的博弈这里说一个开发者必须知道的现实即使JavaScript不能直接访问硬件信息它也可以通过一系列间接信号拼凑出设备的“近似指纹”。比如navigator.userAgent能告诉你浏览器版本和操作系统版本screen.width和screen.height能告诉分辨率navigator.hardwareConcurrency能告诉CPU逻辑核心数navigator.deviceMemory能告诉大致内存大小canvas.toDataURL()能通过GPU渲染差异生成一张几乎唯一的图片指纹。网站把这些信息组合起来准确率可以做到非常高。所以你经常会看到“为什么我清除了Cookie还是被网站识别到了”这类问题。原因就是网站的指纹脚本从各种看似无害的API里收集了几十条特征组合成一个Hash值来标记你。这不是硬件级访问但比硬件访问更难防因为用户完全感知不到。浏览器厂商一方面禁止直接硬件访问一方面也在不停压缩间接指纹的空间。比如navigator.userAgent已经在计划冻结版本号navigator.deviceMemory被加上了粗糙化处理。这就是一场猫鼠游戏安全模型越严格指纹追踪就越需要在沙盒缝里找机会。2.4 权限模型与用户授权的边界那是不是只要经过用户授权JavaScript就能读硬件信息了答案是有限的“是”。浏览器设计了一套权限模型比如navigator.geolocation需要用户点击“允许”navigator.mediaDevices.getUserMedia需要用户允许摄像头和麦克风权限。这类API的特点是第一必须发生在用户手势用户点击、按键之后第二浏览器会弹出明显的授权提示第三API返回的数据范围被严格限定。但就算用户授权了浏览器也没有开放CPU序列号、磁盘序列号、主板UUID这一类系统级标识。因为这类标识属于“高稳定性设备标识”一旦泄漏用户无法通过清理Cookie、重开浏览器窗口来重置等于永久被跟踪。浏览器能接受的授权模型是用户明确知道“我要上传照片或视频所以允许访问摄像头”而不是用户在弹窗里看到“申请获取你的CPU序列号”。后者用户根本无从判断风险所以浏览器直接不做这功能。3. JavaScript并非拿不到任何“硬件线索”能拿到什么不能拿到什么3.1 能拿到的间接线索而且每天都在被用来做指纹识别虽然拿不到真正的硬件ID但JavaScript可以拿到一些跟硬件性能强相关的参数。这些参数单个看没什么用组合起来就是高精度指纹。navigator.hardwareConcurrency是CPU逻辑核心数普通笔记本一般是4到16服务器可能32以上。navigator.deviceMemory是设备内存的近似值Chrome 84开始支持返回的是2、4、8这样的取整值。screen.width和screen.height配合screen.colorDepth和screen.pixelDepth能定出屏幕参数。navigator.platform告诉你操作系统平台。canvas指纹是重头戏同一张图片在不同GPU、不同显卡驱动、不同抗锯齿算法下渲染出来的像素数据会有细微差异这种差异可以被哈希成一个标识。还有一个容易忽略的点window.outerWidth和window.innerWidth的差值可以推算出浏览器工具栏和滚动条的尺寸不同操作系统的默认UI高度不同。这些信息叠加起来足以在百万级别用户里区分出你的设备。下面是个简单示例你可以自己在控制台跑一下看能拿到哪些值function collectHardwareSignals() { const signals { userAgent: navigator.userAgent, platform: navigator.platform || unknown, language: navigator.language, hardwareConcurrency: navigator.hardwareConcurrency, deviceMemory: navigator.deviceMemory || unknown, screenWidth: screen.width, screenHeight: screen.height, screenColorDepth: screen.colorDepth, screenPixelDepth: screen.pixelDepth, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, touchPoints: navigator.maxTouchPoints }; return signals; } console.log(collectHardwareSignals());3.2 明确拿不到的硬件信息别白费力气下面这些信息你在浏览器里用任何方式都拿不到CPU序列号、主板序列号、硬盘序列号、物理MAC地址、内存条的SN号、显卡BIOS版本、电池唯一ID。这些都属于系统级设备标识被操作系统严格保护。你可能会觉得“我明明在Node.js里能拿到”说对了。Node.js不是浏览器环境它跑在服务器或本地进程里没有沙箱所以os.cpus()、os.networkInterfaces()这些API都能直接访问系统信息。但这不是JavaScript语言的能力而是Node.js运行时给你的能力。同样的JS代码放到浏览器里一样会被弹回来。这就是为什么很多人会困惑——同一个语言换个环境能力天差地别。3.3 一张表看清能力边界为了让大家有直观感受我整理了一个能力边界表信息类型能否直接获取说明屏幕分辨率/色深能screen对象直接读取浏览器窗口内宽高能window.innerWidth/innerHeightCPU逻辑核心数能近似navigator.hardwareConcurrency设备内存能近似navigator.deviceMemory操作系统平台能navigator.platform已废弃但可用摄像头/麦克风列表需授权mediaDevices.enumerateDevices地理位置需授权geolocation蓝牙设备列表需授权HTTPSnavigator.bluetoothUSB设备列表需授权HTTPSnavigator.usb电池电量已禁用Battery Status API被移除CPU序列号不能系统级保护硬盘序列号不能系统级保护物理MAC地址不能系统级保护主板UUID不能系统级保护4. 现在的Web标准正在试探“硬件边界”WebUSB、Web Bluetooth都是什么4.1 这些API能做什么看到上面表格里写了navigator.usb和navigator.bluetooth你可能已经意识到JavaScript并不是永远摸不到硬件。WebUSB、Web Bluetooth、Web Serial、Web NFC这些API是近年来浏览器逐步开放的“硬件入口”。它们的设计初衷是解决一个很实际的问题开发者在网页里控制硬件设备。比如你写一个在线烧录工具、一个串口调试面板、一个RFID读卡器控制页传统上只能用本地应用程序做现在浏览器也想分一杯羹。以WebUSB为例它允许网页使用navigator.usb.requestDevice()弹出一个系统级设备选择器用户主动选一个USB设备后网页就能通过USB协议跟设备通信。这意味着如果你有一个自定义的USB硬件设备理论上可以做一个纯网页版的配置工具用户插上设备、点一个按钮、选设备就能读取设备数据。4.2 工程上的安全妥协需要用户手势用户选择设备但请注意这类API跟“读硬件信息”有本质区别。它的安全模型设计得非常保守第一必须通过HTTPS页面才能调用。明文HTTP页面直接用不了这是对协议层面的最低要求。第二必须发生在用户主动手势期间也就是用户点击按钮之后不能在onload里静默调用。第三每次调用requestDevice都会弹出系统级设备选择器用户必须手动选择目标设备网页只能拿到“被选择设备的访问权”拿不到整个系统的设备清单。第四网页拿到的是设备访问权限不是设备唯一标识。多数协议里设备不会把序列号当作开放信息返回即使返回网页也没法用它做跨设备追踪。这套设计背后的逻辑是浏览器可以做“用户主动连接的桥梁”但不做“后台静默收集的鼻子”。如果你的网页让用户主动连接了一个设备那说明用户对你的服务有一定信任基础风险相对可控。4.3 实战写一个最简单的WebUSB读取设备序列号的示例下面这个代码片段展示了怎么在支持WebUSB的浏览器里让用户选择设备并读取基本描述信息async function connectUsbDevice() { if (!navigator.usb) { console.warn(当前浏览器不支持 WebUSB请使用 Chrome/Edge 等 Chromium 内核浏览器); return; } try { const device await navigator.usb.requestDevice({ filters: [] }); await device.open(); if (device.configuration null) { await device.selectConfiguration(1); } await device.claimInterface(device.configuration.interfaces[0].interfaceNumber); console.log(已连接的设备:, device.productName); console.log(设备厂商:, device.manufacturerName); console.log(设备序列号:, device.serialNumber); return device; } catch (error) { if (error.name NotFoundError) { console.log(用户取消选择设备); } else { console.error(连接设备失败:, error); } } }注意这里即使能拿到serialNumber也是在用户主动选择设备之后才拿到的。如果用户不理会弹窗你一根头发都捞不着。这才是浏览器想表达的边界用户决定权优先于网页代码意图。5. 如果你真的需要硬件信息几套可行的替代方案5.1 方案一前端指纹识别方案稳定但别用于强安全场景如果你做风控系统、广告反作弊、登录防撞库想在前端识别一台设备最实际的做法就是基于前面提到的各种间接信号做指纹。我建议用现成库比如FingerprintJS。它会把canvas指纹、WebGL渲染器信息、屏幕参数、AudioContext音频指纹等聚合成一个稳定ID。它的免费版有一个40字节的浏览器指纹稳定率在多数场景下够用。但有几个坑用户在隐私模式、禁用第三方Cookie、更换显卡驱动、系统更新之后指纹可能变化。所以指纹识别的定位应该是“风控辅助信号”而不是“设备唯一凭证”。如果你要用它做“设备绑定”级别的安全控制我劝你再想想。5.2 方案二用Electron/Tauri做桌面端摆脱浏览器沙箱如果你的产品本身就是一个需要读取硬件信息的工具类软件比如硬件检测工具、串口调试助手、网卡管理工具那根本没必要在浏览器里硬扛。直接用Electron或Tauri做成桌面应用。Electron本质上是一个打包了Chromium和Node.js的桌面运行时。你在main进程里可以放心用os.cpus()、os.networkInterfaces()、process.platform也可以引入systeminformation这个npm包它能拿到CPU、内存、硬盘、主板、显卡、电池甚至BIOS版本的详细信息。renderer进程仍然受沙箱限制但你可以通过IPC把main进程获取的数据转发给前端界面显示。下面是Electron里读取系统信息的示例// main.js const os require(os); const { systeminformation } require(systeminformation); const { BrowserWindow, ipcMain } require(electron); ipcMain.handle(get-hardware-info, async () { const [cpu, mem, disk] await Promise.all([ si.cpu(), si.mem(), si.diskLayout() ]); return { platform: process.platform, hostname: os.hostname(), cpuModel: cpu.brand, cpuCores: cpu.cores, memoryTotal: mem.total, diskInfo: disk.map(d ({ model: d.model, size: d.size, type: d.type })) }; }); function createWindow() { const win new BrowserWindow({ width: 1000, height: 700, webPreferences: { nodeIntegration: true, contextIsolation: false } }); win.loadFile(index.html); } app.whenReady().then(createWindow);Tauri是另一个思路它用系统WebView加Rust后端体积更小但环境配置和API设计比Electron更折腾一点。如果团队没人熟悉Rust还是老实选Electron。5.3 方案三先采集到后端再由后端做硬件绑定如果场景是Web端需要做设备绑定比如“这款软件只能在一台设备上激活”更稳妥的做法是后端直接采集设备信息。具体流程是用户安装一个小的桌面代理程序可以是用C#、Go、Rust写的小工具做设备信息采集代理程序把硬件Hash值发送到后端后端把Hash值跟账号绑定Web端每次登录时后端对比当前设备Hash和绑定时的Hash不一致就触发二次验证。这套方案的好处是Web前端完全不需要做硬件信息采集所有敏感逻辑都藏在后端和桌面代理里。缺点是用户要多装一个程序对纯Web产品来说转化率会受影响。但对于高价值产品的防滥用场景这个成本通常是可以接受的。5.4 方案四利用现有API的“无感识别”技巧如果你想在不打扰用户、不装代理的前提下提升设备识别的准确率有些基于现有API的合法技巧可以叠加使用。比如把canvas指纹、WebGL渲染器信息、AudioContext音频指纹、Intl.DateTimeFormat时区信息、CSS Media Queries里的(pointer: fine)、(hover: hover)、(max-device-width)组合起来。这些信息单个看不出什么组合成一个json数组再哈希稳定性会高很多。有一个细节值得注意WebGL的渲染器字段WEBGL_debug_renderer_info能直接返回GPU型号字符串比如“ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Direct3D11 vs_5_0 ps_5_0)”这是目前前眼里最接近“硬件级标识”的信息。但浏览器已经开始对这个字段逐步做模糊化处理Firefox直接返回空值Chrome也在计划限制。你需要在兼容性和精确性之间做权衡。另外注意如果你用了AudioContext指纹生成技术一定要做好用户插拔耳机、切换输出设备后指纹变化的兼容处理。实测下来音频指纹在部分Windows设备上变化率挺高的不适合做唯一主键。6. 日常开发里常见的“跟硬件相关的JS报错与排查实录”6.1 navigator.usb undefined、getUserMedia 权限被拒很多开发者第一次在项目里用WebUSB API控制台直接报navigator.usb is undefined。这个不是BUG是浏览器不支持。Chrome桌面版默认支持但Firefox、Safari都不支持。要兼容不同浏览器你只能做特性检测然后降级处理不能强制用户换浏览器。getUserMedia报NotAllowedError也很常见。排除用户手动拒绝的情况最常被忽略的原因是页面不是HTTPS环境。浏览器要求调用摄像头和麦克风必须在安全上下文里localhost算例外但局域网IP不算。如果你在测试环境被这个问题卡住最快的解决办法是给测试服务器挂一张自签名证书或者直接用localhost测。6.2 HBuilderX里配置HTML/CSS/JS时遇到的脚本模块加载报错很多人用HBuilderX做前端开发配置JavaScript模块时偶尔会遇到failed to load module script: expected a javascript module script but the server responded with a MIME type of text/html这类报错。这跟硬件无关但跟“开发环境到底在跑什么”有关。根本原因是入口页面通过script typemodule src...加载的JS文件被服务器以错误的MIME类型返回了通常是路径配置不对导致请求落到了404页面而不是实际的.js文件。建议在HBuilderX里发布项目之前先检查一下项目结构里的静态资源路径是否跟服务器的虚拟目录对得上。尤其注意相对路径和绝对路径的区别如果你用了/js/main.js这种根路径但项目部署在子目录下就会报这个错。解决办法是统一改用相对路径或者配置正确的base路径。如果你用的是Vite构建还要记得把base: ./配上。这些都是老坑了每次换环境必踩一次。6.3 注册表/Windows驱动提示 “windows无法启动这个硬件设备” 与前端无关搜索关键词里有一条是“由于其配置信息注册表中的不完整或已损坏windows无法启动这个硬件设备”这个报错看起来跟你项目毫无关系但确实有开发者会搜到这里来。这是因为前端页面在调用摄像头或麦克风时如果设备驱动异常浏览器也会报一个“无法启动设备”的错误。我的排查建议是用navigator.mediaDevices.enumerateDevices()先列一下系统里有没有识别到设备。如果结果为空说明浏览器层面就没看到设备大概率是系统驱动问题或权限问题如果结果有设备但getUserMedia失败再去查驱动和注册表。不要让用户盲目去改注册表那是电脑维修工干的活前端能做的就是把错误信息打印清楚告知用户检查设备驱动和浏览器权限即可。6.4 防坑速查表问题可能原因排查方向navigator.usb 不存在浏览器不支持换成Chromium内核浏览器或做特性检测降级getUserMedia 报 NotAllowedError非HTTPS环境、用户拒绝检查页面协议检查浏览器站点权限设置devicePixelRatio 异常多屏缩放设置不同用matchMedia监听变化不要全局缓存取值HBuilderX 下 module script 报错路径错误、MIME类型不对检查加载路径确认静态资源可访问Windows 设备提示无法启动驱动损坏、注册表残留建议用户重装驱动前端只要打日志即可7. 说给学习JavaScript的新手别把“不能访问硬件”当成缺陷7.1 先把基础打牢HBuilderX配置、ES6、filter这些才更值得花时间我见过很多新手学JavaScript上来就想做“读取CPU温度”“控制外设灯”这种看起来很酷的DEMO。但JavaScript这个语言最值钱的不是跟硬件打交道的能力而是它庞大的生态和异步编程模型。你用HBuilderX配置项目环境时是不是经常卡在CSS怎么引入、JavaScript模块怎么加载、浏览器兼容性怎么处理你把ES6的Promise、async/await、数组的filter、map、reduce搞明白再学会事件循环和闭包就已经能处理绝大多数业务逻辑了。这些基础功不扎实就算浏览器明天突然放开硬件API给你你也写不出一套稳定的硬件交互逻辑。7.2 如果你对JS运行时报错和调试还不熟这是你的第一个重点前端的现状是绝大多数问题不是你代码逻辑写错了而是你对环境绑定关系的理解有缺失。Cannot read properties of undefined、TypeError: xxx is not a function、Uncaught SyntaxError: Unexpected token这些报错在运行时突然蹦出来你大概能猜到是异步回调里的this丢了还是JSON解析失败还是某个接口字段跟预期不一致。具体到跟环境相关的调试把浏览器开发者工具的Console、Network、Application三个面板用熟比背十个硬件API都管用。比如你在HBuilderX里改了代码但浏览器不生效先看看是不是缓存了旧的JS文件你再遇到failed to load module script第一反应应该是打开Network面板看资源加载状态而不是去搜“JavaScript报错”。调试思路顺了很多技术难点其实就是一层窗户纸。我自己做前端这些年踩过的最大的坑就是“想直接控制硬件”结果绕了半天路最后发现用Electron反而更快。这不是说浏览器限制是坏事恰恰相反正是这些限制逼着我们设计出更合理的架构前端只做展示和交互真正有权限的部分放到后端或者桌面代理里每个环节只做自己擅长的事整个系统才安全可靠。还有一个个人体会如果你是为了做风控或防作弊永远不要依赖单一维度做设备识别。浏览器指纹是会变的用户换了浏览器、清除了数据、重装系统都会失效。合理的设计是把指纹、账号行为、IP、操作习惯综合起来。如果你正在做WebUSB或者Web Bluetooth这类项目务必把异常处理做得足够细用户取消授权、设备未插好、设备驱动异常每一个场景都该有对应的提示文案。
返回列表