尧图精选

Vue项目中获取客户端主机ID、IP与主机名的完整方案

🕒 发布时间:2026/10/1 23:56:18 📁 来源:尧图网络
做了几年企业内部的系统总会碰到一个有点尴尬的需求要记录一下“是哪台电脑在访问”。最开始我以为是查一下访问者的IP就行后来发现光有IP不够运维那边要求连主机名、甚至主机唯一标识一起拿过来方便资产盘点和对账。于是就有了““PC端获取主机id、IP地址、主机名””这个需求。这个项目本身不复杂但在Vue JS的纯前端项目里想直接拿到客户端的主机信息完全不是调一个API那么简单。这篇文章就把我实际趟过的路、选型的思考和踩过的坑完整写出来给同样要做“设备识别”“访问来源标记”的朋友一个可以参考的落地方案。先说结论纯前端JS拿不到完整的主机信息只能通过WebRTC碰运气拿到内网IP真正稳定、通用的方式是“前端 后端配合”。如果你只是临时看一下自己的IP那WebRTC方案完全够用但如果要做资产记录、设备绑定、权限校验这类正经事就必须让后端介入。1. 内容整体设计与思路拆解1.1 需求本质浏览器到底能给我们什么在开始写代码之前必须先弄明白一个底层逻辑浏览器为什么不能直接读取客户端的主机名和主机ID这个问题的根源在浏览器的安全模型。页面里跑的JavaScript运行在一个被高度隔离的沙箱里它原则上不允许访问操作系统层面的信息。你想想如果一个网页能随便读取你电脑的“机器唯一编号”“硬盘序列号”那网站不就等于可以偷偷追踪每一台设备了所以浏览器设计者从底层就堵死了这条路window对象上没有hostname、没有deviceId你翻遍所有Web API都找不到一个获取“本机主机名”的方法。那IP呢其实普通JavaScript也拿不到——以前有人用XMLHttpRequest请求一个外部服务来获取出口IP但那拿到的是经过NAT转换后的公网IP不是本机的网卡IP。真正能拿到内网IP的办法是借助WebRTC这家伙它为了建立P2P连接必须收集本地网络候选地址ICE Candidate里面就藏着网卡IP。但WebRTC是一个妥协产物浏览器厂商后来为了隐私策略又把这条路渐渐堵上了后面我会细说。所以你一开始就应该有清醒的预期纯前端方案上限很低后端方案才是“正餐”。本文的实际开发路径也是冲着一个目标去的在Vue单页应用里通过一个Node.js后端接口把客户端的IP、主机名、主机ID收集回来再展示到页面上。这个设计对于考勤系统、工单系统、内网资产管理、设备授权这类PC端业务是通用的。1.2 主机ID到底是什么别搞混了在项目里“主机ID”是需求方最含糊的词。有人以为是IP有人以为是主机名有人以为是MAC地址。我在项目启动前专门和业务方对过一次参数最终圈定了方向。“主机ID”在大多数企业场景里指的是能唯一标识一台物理电脑的编号常见的有这么几类标识获取方式特点主机名Hostname系统命令hostname可能重复用户可改不可作唯一键MAC地址网卡属性可伪造有多块网卡时需选择主板序列号 / BIOS序列号wmic/dmidecode较稳定但部分组装机可能为空系统机器IDLinux/etc/machine-id、Windows 注册表重装系统后变化硬件UUIDWindowswmic csproduct get uuid比较理想的业务主键虚拟机里也稳定我在实际项目中主机ID用的是“主板产品UUID”Windows/machine-idLinux原因是它在重装系统之后基本不变而且企业PC大多是品牌机这个值存在且唯一。你也可以根据你们IT部门的资产编号规则来定但建议不要在需求阶段含糊一定要落到“具体取哪个字段”的层面否则后面没人能验收。1.3 为什么最终选了“后端辅助”而不是“纯前端”或“桌面端外壳”技术选型时要考虑的因素主要有三个能不能拿到数据、部署成本高不高、兼容性好不好。纯前端方案的问题很明显拿不到主机名和主机ID连IP在Chrome新版里都开始隐藏了。桌面端方案比如Electron、Tauri倒是能拿到所有信息因为Node.js环境可以执行系统命令但代价是要把Web系统“包一层壳”部署、更新、跨平台打包的成本全都上来了。如果你们公司内部业务系统本来就是浏览器访问的让所有用户换用桌面客户端推进阻力会非常大。所以我选了中间路线前端负责展示和交互后端负责采集和输出。后端可以对所有客户端透明用户该怎么打开浏览器还是怎么打开业务逻辑也不变只需要新增一个接口即可。前端通过fetch或axios请求这个接口拿到数据渲染即可。这种方案在技术栈不变的前提下把所有“脏活”都放到了后端是我个人最推荐的做法。2. 核心细节解析与实操要点2.1 纯前端WebRTC方案能拿IP但只能到这虽然我最终没用纯前端方案做正式功能但在方案验证阶段还是把它实现了。WebRTC拿IP的原理很简单RTCPeerConnection建立连接时浏览器会收集本机所有网卡的IP地址作为ICE候选我们只需监听icecandidate事件从候选信息里把IP字段提取出来。function getLocalIPs() { return new Promise((resolve) { const ips []; const pc new RTCPeerConnection({ iceServers: [] }); pc.createDataChannel(); // 必须有数据通道 pc.createOffer() .then((offer) pc.setLocalDescription(offer)) .catch((err) reject(err)); pc.onicecandidate (ice) { if (!ice.candidate) { // 候选收集完毕 resolve([...new Set(ips)]); return; } const ipRegex /([0-9]{1,3}(\.[0-9]{1,3}){3})/; const match ice.candidate.candidate.match(ipRegex); if (match) { ips.push(match[1]); } }; }); }这段代码的核心逻辑是创建连接、创建Offer、监听候选收集事件。需要说明的是iceServers空数组意味着不走STUN服务器只在本地收集速度很快不需要外网。这套方案运行下来你通常能拿到192.168.x.x、10.x.x.x这类内网地址。但存在几个很实际的问题Chrome从93版本开始默认启用了mDNS隐私保护返回的候选地址可能是xxxx.local这种形式根本解不出真实IP。Firefox对WebRTC候选地址做了混淆处理拿到的基本就是乱码或localhost。拿不到主机名和主机ID这是硬伤。结论WebRTC方案适合开发调试、展示自己地址用不适合做正式的业务数据采集。2.2 后端方案设计Node.js如何拿到客户端信息后端接口的设计思路一句话就能说清前端把请求发过来后端在请求上下文中解析来源IP再通过系统命令读取主机名和主机ID最后组装成JSON返回。这里有几个关键点需要展开讲。第一个关键点是“客户端的真实IP”。在直接连接模式下从socket上就能拿到IP比如Node.js的req.socket.remoteAddress。但企业应用常有反向代理Nginx、IIS ARR此时直接拿到的就是代理服务器的IP了。正确处理方式是解析X-Forwarded-For头取第一个合法的IP。function getClientIp(req) { const forwarded req.headers[x-forwarded-for]; if (forwarded) { const first forwarded.split(,)[0].trim(); if (isValidIP(first)) return first; } return req.socket.remoteAddress?.replace(::ffff:, ) || ; }isValidIP最好用net.isIP判断一下不要直接信任请求头里的值防止伪造和异常数据。第二个关键点是“如何拿主机名和主机ID”。浏览器不能做但Node.js可以调用系统命令因为服务器端进程是运行在操作系统上的。在Windows下用wmic或PowerShell在Linux下用cat和hostname。为了避免不同平台写两套逻辑我写了一个轻量的命令分发函数const os require(os); const { execSync } require(child_process); function getSystemInfo() { const platform os.platform(); let hostname os.hostname(); // 服务器主机名不是客户端 let hostId ; if (platform win32) { try { hostId execSync(wmic csproduct get uuid, { encoding: utf8 }) .split(\n)[1]?.trim() || ; } catch (e) { hostId ; } } else if (platform linux) { try { hostId execSync(cat /etc/machine-id, { encoding: utf8 }).trim(); } catch (e) { hostId ; } } else if (platform darwin) { try { hostId execSync(ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID, { encoding: utf8 }) .split()[3] || ; } catch (e) { hostId ; } } return { hostname, hostId }; }这里需要特别指出的是这段代码在“后端服务器上”执行拿到的hostname和hostId是“服务器”的不是“客户端”的。最初我做原型时在这里绕了个大弯子以为这样就可以拿到用户电脑的信息结果返回的一直是自己的服务器信息。这是一个必须想清楚的架构问题如果系统是前后端部署在同一台电脑上比如单机版软件后端就跑在用户电脑上那上面的方案就能拿到客户端的机器信息。如果系统是中央服务器模式用户浏览器访问远程服务器那后端命令只能拿到服务器信息拿不到用户电脑信息。对于第二种更常见的情况正确的方案是前端放一个“采集Agent”或通过“下载工具脚本”在客户端执行然后把结果上报或者直接降级为只记录用户IP。我在项目里用的是“前端通过后端接口上报所在内网IP”的方式因为我们的业务限定在办公网内允许我额外封装一个“内网探测接口”来绕过跨域限制。后面第3章的实战链路会默认这种“内网环境下的中央服务器 前端上报内网IP”模式展开。2.3 前端Vue侧实现要点Vue侧的核心工作是“请求接口 状态管理 渲染”。我用一个useSystemInfo的Composable来封装Vue 3写法但Vue 2的Options API同样适用只是把方法和数据放进data和methods里import { ref } from vue; import axios from axios; export function useSystemInfo() { const loading ref(false); const error ref(); const systemInfo ref({ clientIp: , hostname: , hostId: , }); const fetchSystemInfo async () { loading.value true; error.value ; try { const { data } await axios.get(/api/system-info, { timeout: 5000 }); systemInfo.value data.data || data; } catch (e) { error.value e.message || 获取系统信息失败; } finally { loading.value false; } }; return { loading, error, systemInfo, fetchSystemInfo }; }在组件里使用这个组合式函数把数据绑定到表格或表单隐藏域里即可。这里有两个小经验想分享接口地址最好走代理或相对路径避免前后端分离时出现跨域问题。生产环境由Nginx统一转发开发环境用Vite的proxy配置。获取过程可以做成“静默获取”页面加载后自动调用不阻塞用户的正常操作。如果超时或失败就显示提示但不要阻止进入系统。因为设备信息采集毕竟是辅助功能不能变成系统的“单点故障”。3. 实操过程与核心环节实现3.1 后端接口完整实现Node.js Express前端需要的无非是一个标准JSON接口。我以Node.js Express为例给出一个可以直接抄作业的完整后端。如果你用的是Java、Python、Go思路完全一致只是换成对应的进程调用方式。先初始化项目并安装依赖npm init -y npm install express新建server.js完整代码可以这样写const express require(express); const os require(os); const { execSync } require(child_process); const net require(net); const app express(); app.use(express.json()); // 获取客户端真实IP function getClientIp(req) { const forwarded req.headers[x-forwarded-for]; if (forwarded) { const first forwarded.split(,)[0].trim(); if (net.isIP(first)) return first; } return req.socket.remoteAddress?.replace(::ffff:, ) || req.ip || ; } // 获取主机名和主机ID function getSystemInfo() { const platform os.platform(); let hostname ; let hostId ; try { if (platform win32) { hostname execSync(hostname, { encoding: utf8 }).trim(); hostId execSync(wmic csproduct get uuid, { encoding: utf8 }) .split(\n)[1]?.trim() || ; } else if (platform linux) { hostname execSync(hostname, { encoding: utf8 }).trim(); hostId execSync(cat /etc/machine-id, { encoding: utf8 }).trim(); } else if (platform darwin) { hostname execSync(scutil --get ComputerName, { encoding: utf8 }).trim(); const uuidLine execSync(ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID, { encoding: utf8 }); hostId uuidLine.match(/IOPlatformUUID ([^])/)?.[1] || ; } } catch (e) { // 单条命令失败不影响其他字段 } return { hostname, hostId }; } // 获取内网IP列表 function getLocalIPs() { const interfaces os.networkInterfaces(); const list []; for (const name of Object.keys(interfaces)) { for (const iface of interfaces[name]) { if (iface.family IPv4 !iface.internal) { list.push({ name, address: iface.address, mac: iface.mac, }); } } } return list; } app.get(/api/system-info, (req, res) { const clientIp getClientIp(req); const { hostname, hostId } getSystemInfo(); const localIPs getLocalIPs(); res.json({ code: 0, data: { clientIp, hostname, hostId, localIPs, serverTime: Date.now(), }, }); }); app.listen(3000, () { console.log(system-info server listening on 3000); });这套接口返回的数据结构我拆解一下clientIp客户端访问我们的来源IP经过代理后也能取到第一个有效IP。hostname、hostId后端所在机器的信息。再次强调单机模式时它们就是客户端信息中央服务器模式时它们是服务器信息业务设计上要区分清楚。localIPs后端机器的网卡IP列表主要是排障时看网络用的。为了安全建议给这个接口加一个简单的鉴权比如要求请求头携带一个自定义Tokenapp.use(/api/system-info, (req, res, next) { const token req.headers[x-token]; if (token ! your-internal-token) { return res.status(403).json({ code: 403, message: forbidden }); } next(); });3.2 前端Vue页面集成接着在Vue组件里展示获取到的信息。下面是一个带基础样式的示例template div classsystem-info-card h3访客设备信息/h3 div v-ifloading正在获取设备信息.../div div v-else-iferror classerror获取失败{{ error }}/div ul v-else li访问IP{{ systemInfo.clientIp }}/li li主机名{{ systemInfo.hostname }}/li li主机ID{{ systemInfo.hostId }}/li li 本机网卡列表 span v-foritem in systemInfo.localIPs :keyitem.address {{ item.name }} ({{ item.address }}) /span /li /ul button clickfetchSystemInfo :disabledloading {{ loading ? 获取中... : 重新获取 }} /button /div /template script setup import { onMounted } from vue; import { useSystemInfo } from ./composables/useSystemInfo; const { loading, error, systemInfo, fetchSystemInfo } useSystemInfo(); onMounted(() { fetchSystemInfo(); }); /script页面加载后自动调用fetchSystemInfo用户也能手动刷新。如果你们业务需要在提交表单时把这几个字段带上就把systemInfo对象挂到表单提交数据里const submitForm async () { await fetchSystemInfo(); formData.deviceIp systemInfo.value.clientIp; formData.deviceHostname systemInfo.value.hostname; formData.deviceHostId systemInfo.value.hostId; // 继续提交 };3.3 内网IP获取的参数选择与过滤逻辑后端获取网卡IP时有一个很容易被忽视的细节os.networkInterfaces()会返回所有网卡包括虚拟网卡VMware、VirtualBox、Docker网卡、WSL虚拟网卡等。如果你不加过滤就展示用户会看到一长串奇怪的IP比如172.17.0.1、192.168.56.1这种业务方根本不知道哪个才是“真正的IP”。为了实现更精确的过滤我建议按如下优先级选择排除internal为true的回环地址排除常见的虚拟网卡名称比如VMware、VirtualBox、vEthernet、docker、veth等优先选择192.168.x.x或10.x.x.x因为这通常是办公网内用户真实所在网段把剩余地址按网卡名称排序作为列表展示。Node.js里可以用这样的函数做匹配function getPrimaryLocalIP() { const interfaces os.networkInterfaces(); const virtualKeywords [vmware, virtualbox, vethernet, docker, veth, wsl]; const candidates []; for (const name of Object.keys(interfaces)) { const lowerName name.toLowerCase(); if (virtualKeywords.some((kw) lowerName.includes(kw))) continue; for (const iface of interfaces[name]) { if (iface.family IPv4 !iface.internal) { candidates.push({ name, address: iface.address }); } } } candidates.sort((a, b) { const score (ip) { if (ip.startsWith(192.168.)) return 2; if (ip.startsWith(10.)) return 1; return 0; }; return score(b.address) - score(a.address); }); return candidates[0] || null; }实际项目里我遇到过“一个办公室有两层NAT”“一条网线插在多个交换机上”导致出现多个候选IP的情况但最终用户电脑的默认网关所在网段一定是可用的。如果你需要更精准的结果可以在后面再调一次arp -a命令把默认网关的ARP表拉出来匹配物理地址所在的网卡。但这对普通业务来说有些过度设计上面的排序策略已经够用了。4. 常见问题与排查技巧实录4.1 Chrome下WebRTC拿不到真实IP怎么办如果你出于调试目的使用纯前端WebRTC方案会发现Chrome里返回的候选地址是xxxx.local格式解出来的IP可能是192.168.1.102但也有可能是[0:1:...]这种奇怪的URI。原因就是Chrome 93之后默认启用了mDNS它会把真实IP隐藏成一个随机域名。有几种应对办法在Chrome启动参数里加--disable-featuresWebRtcHideLocalIpsWithMdns但这只对本地调试有效不能要求所有用户改启动参数。换用Firefox调试但Firefox对WebRTC的IP策略更激进经常直接返回空候选。通过getUserMedia触发权限弹窗用户同意后有机会拿到真实IP但这会造成“拿个IP还要弹窗授权”的糟糕体验。所以结论还是那句话如果要进正式业务流程别在纯前端方案上死磕直接走后端。WebRTC只适合你自己写小工具的时候用。4.2 后端拿到的IP为什么总是服务器地址而不是用户地址这是问得最多的问题。很多人照着“后端获取IP”的教程写完接口测试时却发现返回的是内网服务器的10.0.0.8而用户电脑明明是192.168.50.23。这就是我前面反复强调的“命令在哪台机器上执行拿到的就是哪台机器的信息”。要拿到用户电脑的信息可行的路径只有两种系统带客户端Agent在用户电脑上安装一个小程序如果你们公司本身就是C/S架构Agent采集后上报。纯B/S架构只能做降级后端只能记录“来源IP”无法记录真正的客户端主机名和主机ID。不过对于大多数管理系统的场景“来源IP 时间 账号”已经能起到很好的审计作用了。如果你们的业务必须拿客户端主机名、主机ID但又不允许安装任何客户端那基本没有合法合规的浏览器方案。这时需要和业务方重新对齐预期提供“手动填写”的兜底入口或者在内网可控环境里推一个“浏览器扩展”但扩展的维护成本更高并不适合轻量业务。4.3 多网卡和虚拟网卡导致IP列表混乱测试过程中我发现os.networkInterfaces()返回的结果里混杂了大量无关地址。比如装了Docker Desktop后会多出好几个192.168.65.x、172.17.0.1之类的网卡地址装了VMware Workstation还会看到192.168.56.1。如果直接把列表展示给用户体验非常糟糕。我的处理方案是做一个“网卡账户”存进本地存储{ filtered: [ { name: 以太网, address: 192.168.11.108 } ], ignored: [ { name: VMware Network Adapter VMnet8, address: 192.168.56.1 } ] }然后在前端展示时只展示过滤后的主地址。如果用户反馈“我的IP不对”再打开完整列表让用户自己选。另外还有一个注意点ipconfig展示的“物理地址”不要乱用。MAC地址涉及用户隐私而且多网卡时用户自己都分不清哪块网卡对应哪条网线强行展示反而增加困扰。4.4 主机ID获取失败的空值情况Windows上执行wmic csproduct get uuid大多数时候没问题但我在两台自组装的电脑上遇到输出为空原因是主板信息里没有写入UUID。Linux下/etc/machine-id理论上是系统安装时自动生成的极少为空。macOS的IOPlatformUUID偶发读取失败。我的兜底策略是三级首选“产品UUID / machine-id / IOPlatformUUID”拿不到时退一步取“MAC地址 主机名”拼接后的哈希值作为临时ID还是拿不到时把字段置为空字符串由前端提示用户手动填写。这里要特别提醒一句主机ID不能作为强认证凭据它只适合做“设备画像”和“审计标记”不适合做授权依据。真要做设备绑定建议结合其他认证方式一起使用。4.5 常见问题速查表现象大概率原因解决方案WebRTC拿到的IP是.local域名Chrome的mDNS隐私保护使用后端方案或临时关掉mDNS调试后端返回的hostname是服务器名字后端没有跑在客户端机器上明确架构或引入客户端Agenthostname和hostId字段为空系统命令执行失败或权限不足增加兜底逻辑返回空字符串接口跨域报错前后端分离端口不一致开发用Vite代理生产用Nginx转发获取IP耗时太长走了STUN或系统命令超时用空iceServers设置5秒超时展示的IP列表有一堆172.xDocker/WSL虚拟网卡按关键词过滤虚拟网卡5. 工具选型与部署注意事项5.1 技术栈选型的取舍这个项目用Vue JS是没问题的但如果你有选择权我建议后端不要用和前端一样的Node.js而是看你们公司现有的运维栈。如果是Java技术栈用Spring Boot执行命令的方式更成熟如果是Pythonsocket操作和subprocess也是很顺手的工具。不过Node.js有一个独特的优势它和前端可以共用一套TS类型定义接口字段不容易对不上。我在写这个项目时把后端返回的数据结构和前端useSystemInfo里的接口类型定义放在同一个目录下前后端都引用它字段名保持完全一致省了很多联调时间。5.2 生产环境的部署细节接口做好之后部署时要考虑几件事代理配置Nginx里要对/api/system-info进行转发不要把后端端口暴露到公网。同时要保证X-Forwarded-For传递链完整否则后端拿不到用户真实IP。超时设置execSync是同步命令如果是Windows下wmic的话首次运行可能稍慢。建议给后端接口整体加一个3到5秒的响应超时。如果超过时间就返回部分数据不要让用户一直转圈。请求限流设备信息接口不需要频繁调用可以按IP限制每分钟调用次数。我用了一个简单的内存计数器超过阈值就返回429。日志记录建议把“来源IP、时间、访问路径、User-Agent”打在访问日志里万一事后追查设备问题有一个底账。5.3 前端打包后“接口地址不对”的坑Vue项目开发时走Vite代理一切正常打包部署后如果把静态文件直接双击打开file://协议接口自然请求不到。正确做法是把静态资源放到Nginx里并配置同源反向代理。如果你们是前后端彻底分离后端接口部署在另一个域名那必须给后端配置CORS前端设置好baseURL。这里有一个容易踩的坑打包后的文件放在CDN或用对象存储托管时接口地址要用全路径不要用相对路径/api/system-info否则在路由的非根路径下刷新页面相对路径会变成/sub/api/system-info直接404。我一般都是用一个运行时配置const API_BASE window.__RUNTIME_CONFIG__?.API_BASE || /api; // 请求时使用 ${API_BASE}/system-info这样Nginx、容器化部署、CDN部署都能灵活切换不会因为打包把地址写死。6. 从“拿到数据”到“用得顺手”的经验沉淀整个项目从需求评审到上线我自己最大的感受是技术实现只占三成功夫另外七成都在“定义主机ID”和“对齐部署架构”上。特别是第一次做这类功能的时候很容易陷进“前端能不能直接拿主机名”这个技术细节里和浏览器安全模型较劲半天。后来我养成了一个习惯接到类似需求先画一张简单的链路图用户浏览器 - 谁提供数据 - 数据传给谁 - 存到哪里。把数据的“来源”和“去向”标清楚自然就知道哪些环节需要后端、哪些环节可以降级。在Vue侧我也建议把“获取设备信息”抽成一个独立的组合式函数或混入不要散落在页面里。因为这类数据往往在多个地方用到登录日志、工单记录、操作审计、后台展示每个页面都重新写一遍很容易出Bug。抽出useSystemInfo之后后续需要加字段比如加一个登录用户名、加一个浏览器语言都只改一个文件就行。另外一个小技巧是把接口返回的数据缓存到sessionStorage里下次进入页面时先展示缓存再异步刷新。好处是用户的等待时间大幅减少而且即使后端接口临时抖动界面上也不会直接出现一大片空白或报错。最后再分享一个实际遇到的坑在Windows上执行wmic命令时部分机器会输出“Node - 无法获取信息”这类中文错误信息而不是正规的UUID。一开始我没做异常拦截直接把整段命令输出塞给了前端导致页面渲染出奇怪的内容。后来在处理时先按行拆分再用UUID格式正则校验一次不符合就置空问题才解决。所以无论用哪个命令拿到结果后最好都做一次“数据清洗”它和业务数据一样需要“脏数据处理”。这个项目目前已经稳定跑了半年多每一天的访问来源、设备标识都会汇总到统计报表里运维那边终于不用再一台一台翻交换机日志了。如果你正好在处理类似需求建议就从第3章的后端接口开始抄作业然后根据你们公司的系统架构把命令换成合适的平台版本。纯前端的WebRTC方案留着当玩具就好别真拿去上线。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →