简介针对前端开发中需要获取客户端IP、MAC与主机名的实际需求内容整理成一套可对照查阅的PDF文档适合JavaScript开发者和需要做访问来源定位、个性化展示或简单安全校验的网页工程师。文档围绕7种实现路径展开既包括IE下通过ActiveX对象读取本机信息的方案也覆盖调用新浪、搜狐、太平洋等第三方接口获取公网IP并介绍了WebRTC获取内网IP及HTTP头部转发IP的用法同时说明获取MAC和主机名通常需要插件或受限环境。资源包共1个PDF文件约56KB便于快速下载后直接阅读。截至目前已有2054人学习使用对于想避开网上零散失效代码、系统了解各方案兼容性与局限性的读者这份汇总能减少踩坑并给出在不同浏览器和服务端代理场景下的取舍参考。1. JS获取客户端IP/MAC/主机名7个方法里真正能跑的只有这几条做Web监控、内网资产管理、JS反爬识别的人大概率都搜过“JS获取IP地址、JS获取MAC、JS获取主机名”这类问题。我先摆个结论纯浏览器环境里公网IP有稳定取法内网IP能靠WebRTC掏出来但新版Chrome会做mDNS混淆而MAC地址在浏览器沙箱里基本拿不到必须换Electron或旧IE的ActiveX路径。这些限制不是JS不行是浏览器安全模型故意堵死的。这篇按7个方法逐个拆附带参数细节和排查记录适合两类人——一类是做内网终端信息采集的一类是搞反爬对抗、需要识别真实设备环境的。2. 公网IP与内网IP两条互补的取法与参数细节2.1 公网IPfetch第三方API与JSONP两个分支公网IP的拿法最直接因为浏览器天生允许跨域请求一部分公开API。常见做法是让前端直接请求一个IP查询服务拿JSON回来解析。我一般优先用 ipify它返回体小、支持CORS页面不用走代理。fetch(https://api.ipify.org?formatjson) .then(res res.json()) .then(data { console.log(公网IPv4:, data.ip); }) .catch(err console.error(获取公网IP失败:, err));这段代码的要点在format参数。传formatjson时返回{ip:1.2.3.4}不传则直接返回纯文本IP。注意api.ipify.org响应头带Access-Control-Allow-Origin: *这是它能在浏览器里直接fetch的前提如果换成不自带CORS的接口请求会被跨域策略拦掉。另一个分支是JSONP适合页面还跑在HTTP老环境、或者目标浏览器不支持fetch的场景。JSONP利用script标签不受同源策略限制这个特性让服务端把数据包在一个回调函数里返回function getPublicIPByJSONP(timeout 3000) { return new Promise((resolve, reject) { const script document.createElement(script); const timer setTimeout(() { script.remove(); reject(new Error(JSONP请求超时)); }, timeout); window.__ipCb function (data) { clearTimeout(timer); script.remove(); delete window.__ipCb; resolve(data.ip); }; script.src https://api.ipify.org?formatjsonpcallback__ipCb; document.head.appendChild(script); }); }JSONP的坑在回调函数名和超时清理。callback参数指定全局函数名服务端会把IP作为参数回填进来如果忘了删script或者忘了清理定时器页面会残留脏节点。我习惯在callback里先clearTimeout再script.remove()避免内网终端网络慢时悬浮脚本堆积。另外这类方案会往外网打流量如果公司在内网部署了自己的IP查询服务建议优先走内部地址公网接口只留作兜底。2.2 内网IPWebRTC 的 ICE candidate 挖掘与 mDNS 干扰内网IP比公网IP麻烦因为浏览器不会主动暴露本机IP。WebRTC在建立连接时为了让ICE协商能通会把本机可用的candidate列表暴露出来里面就包含本机网卡的IP。这个思路至今可用。function getLocalIPs() { return new Promise(resolve { const pc new RTCPeerConnection({ iceServers: [] }); pc.createDataChannel(probe); pc.createOffer().then(offer pc.setLocalDescription(offer)); const ips []; pc.onicecandidate event { if (!event.candidate) { pc.close(); resolve(ips); return; } const parts event.candidate.candidate.split( ); // candidate 格式: 第4位是地址第7位是候选类型 if (parts[7] host) { ips.push(parts[4]); } }; }); }两个关键参数必须强调。第一iceServers要传空数组。如果配了STUN浏览器会尝试去公网打洞candidate里就会混入srflx类型的公网IP反而干扰内网IP的判断。我一般先关掉STUN拿内网需要公网时再单独处理。第二parts[7] host是过滤条件只保留本地候选。在老版本Chrome和Firefox里parts[4]就是192.168.x.x这类内网地址但Chrome 80以后默认启用mDNS隐藏本地IPparts[4]会变成rtc-xxxx.local这种混淆名章节3.3会专门说这个问题。多网卡的机器在走这个方案时会返回多个IP因为每个网卡都会生成host候选。拿到结果后先做一轮私有网段过滤把128.x、169.254.x这些APIPA地址丢掉只保留10、172.16、192.168开头的后面验证章节会给现成的过滤函数。3. 主机名与MAC浏览器禁区与Electron/ActiveX的路径3.1 Electron 环境os 模块一条龙拿MAC、内网IP和主机名Electron是这7个方法里数据最全的。它本质上是Node.js加Chromium所以能同时拿到浏览器拿不到的MAC和主机名。做法是用os.networkInterfaces()枚举网卡再用os.hostname()取机器名。const os require(os); const interfaces os.networkInterfaces(); const macSet new Set(); const hostname os.hostname(); for (const name of Object.keys(interfaces)) { for (const detail of interfaces[name]) { if (detail.family IPv4 !detail.internal) { console.log(适配器: ${name}); console.log(内网IP: ${detail.address}); if (detail.mac detail.mac ! 00:00:00:00:00:00) { macSet.add(detail.mac); } } } } console.log(主机名:, hostname); console.log(MAC列表:, [...macSet]); // 单个网卡信息示例 console.log(interfaces[eth0]);family只有IPv4和IPv6两个取值internal为true的是loopback回环地址必须跳过。detail.mac是网卡物理地址但虚拟机场景里它会变成虚拟MAC比如VMware常见00:0C:29开头Hyper-V常见00:15:5D开头。如果你在做终端资产识别判断虚拟机时不能只靠hostname要结合MAC的OUI Vendor前缀一起看。同一个物理网卡在networkInterfaces()里会出现两条记录一条IPv4一条IPv6它们的mac字段是相同的所以用Set去重。os.hostname()的返回值依赖系统设置Windows是计算机名Linux是/etc/hostname的内容在装机工具镜像里往往是一串随机字符。遇到这种情况主机名只能当辅助字段不能当唯一标识。3.2 旧IE的ActiveX用WMI一次取完IP、MAC和DNSHostNameIE配合ActiveX是唯一一条在“浏览器页面”里直接拿MAC的老路靠的是WMI查询。很多政企内网的管理系统还在用IE兼容模式这条路径至今仍有存在意义。function getNetworkInfoByWMI() { if (typeof ActiveXObject undefined) { throw new Error(当前浏览器不支持 ActiveX); } var locator new ActiveXObject(WbemScripting.SWbemLocator); var service locator.ConnectServer(., root\\cimv2); var items service.ExecQuery( SELECT MACAddress, IPAddress, DNSHostName FROM Win32_NetworkAdapterConfiguration WHERE IPEnabledtrue ); var e new Enumerator(items); for (; !e.atEnd(); e.moveNext()) { var item e.item(); console.log(IP:, item.IPAddress(0)); console.log(MAC:, item.MACAddress); console.log(主机名:, item.DNSHostName); } }这段代码有三个前提浏览器必须是IE内核且开启ActiveX页面必须在受信任站点查询Win32_NetworkAdapterConfiguration需要本机权限不能放在低权限的iframe里。WMI返回的IPAddress是数组下标0是IPv4如果终端配了多个IP典型场景是DHCP之外又手动加了第二个地址下标1、2会依次排开。DNSHostName是网卡注册在DNS里的主机名和内网域环境一致时等于系统hostname如果设备在工作组环境两者可能对不上取数时要留意。ActiveX是危险接口不能对任意公网页面开放。做内网管理系统时我会把页面限制在受信任站点并且只允许在管理员认证后的会话里调用避免被外部脚本捡漏。3.3 WebRTC mDNS唯一浏览器直取“主机名”的办法但拿不到真实名Chrome隐藏IPv4后WebRTC的host候选会用mDNS名代替真实IP。这个mDNS名形如9f0d1d7c-xxxx.local虽然带local后缀但它不是真实主机名是浏览器在每次会话里随机生成的混淆标识。function getMdnsHostname() { return new Promise(resolve { const pc new RTCPeerConnection({ iceServers: [] }); pc.createDataChannel(probe); pc.createOffer().then(offer pc.setLocalDescription(offer)); pc.onicecandidate event { if (event.candidate) { const parts event.candidate.candidate.split( ); if (parts[4] parts[4].endsWith(.local)) { console.log(mDNS标识:, parts[4]); pc.close(); resolve(parts[4]); } } }; }); }这个方法的定位很明确它不是用来拿真实主机名的而是用来判断“当前浏览器是否隐藏了本地IP”。做反爬识别或防关联系统时如果同一台机器两次会话里出现了不同的.local后缀说明它每次都在换新身份标识这是一条很隐蔽的关联线索。反过来如果某个采集脚本在Chromium环境里拿到了内网IP而没有.local大概率这台机器被关闭了mDNS隐藏策略这种环境本身就是异常信号。真实主机名的获取始终只有两条路Electron的os.hostname()或者ActiveX的DNSHostName。网上说纯JS能拿到主机名的帖多数是把mDNS混淆名当真实主机名在忽悠踩下去就是坑。4. 后端配合方案WebSocket对端IP与可信HTTP头的取舍4.1 WebSocket 连接从服务端拿真实对端IP前端JS能碰到的IP天花板就是内网IP到了公网出口之外必须靠服务端配合。最有说服力的做法是建立WebSocket连接服务端从TCP连接对端拿地址。这个IP是网络栈实打实给的不依赖前端传参比HTTP头好验真。npm install wsconst WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); wss.on(connection, (ws, req) { const raw req.socket.remoteAddress || ; const ip raw.replace(::ffff:, ); console.log([ws-client-ip], ip); // 内网资产登记拿到IP后去交换机ARP表查MAC ws.send(JSON.stringify({ event: register, clientIp: ip })); }); wss.on(listening, () { console.log(WebSocket 服务已启动监听 8080); });服务端拿到的remoteAddress是IPv6格式时前缀带::ffff:这是IPv4映射地址的表示法用replace去掉前缀再落库。这个方案在内网终端管理里最好用前端打开页面后自动连WebSocket服务端记录到终端IP再配合交换机ARP表做IP到MAC的映射。要注意如果入口有Nginx反代remoteAddress会变成Nginx的地址这时要给Nginx开proxy_protocol或者配置X-Real-IP让原始IP能穿透进来。4.2 HTTP 头场景X-Forwarded-For 的伪造边界与代理白名单HTTP场景下最常翻车的是X-Forwarded-For。很多初级实现直接取req.headers[x-forwarded-for].split(,)[0]客户端随手改一个header就能伪造IP安全策略直接破防。正确做法是只信任直连代理传来的头并且要从右往左解析整个代理链。const trustedProxies new Set([127.0.0.1, ::1, 10.0.0.0/8]); function isIpInTrustedRanges(ip, ranges) { return ranges.has(ip); } function clientIpFromRequest(req) { const direct (req.socket.remoteAddress || ).replace(::ffff:, ); if (!isIpInTrustedRanges(direct, trustedProxies)) { return direct; } const xff req.headers[x-forwarded-for]; if (!xff) return direct; const chain xff.split(,).map(s s.trim()); // 从右往左跳过可信代理取第一个非可信IP for (let i chain.length - 1; i 0; i--) { if (!isIpInTrustedRanges(chain[i], trustedProxies)) { return chain[i]; } } return direct; }核心规则是先看直连对端IP在不在可信代理列表里在才读到XFF读XFF时从最右边往前找越过所有可信代理停在第一个非可信地址上。这是“最后一个可信代理后面紧跟的那个IP”才是真实客户端IP的标准解。另外User-Agent这种自报信息完全不能用来判断客户端身份。很多反爬脚本喜欢拼UA假装成不同浏览器服务端如果把UA和IP绑定作为策略依据会被刷得很难看。IP只能用网络层数据说话UA只做参考。5. 避坑排查MAC获取最容易翻车的5个现场5.1 浏览器直接跑Electron代码终端报 is not defined现象在Chrome控制台里执行os.networkInterfaces()立刻抛 ReferenceError。原因os是Node.js内置模块浏览器沙箱根本没有。Electron的渲染进程如果没开NodeIntegration同样拿不到。解决先判断运行环境再决定走哪条取数路径。判断条件看typeof process ! undefined process.versions.node同时满足才走Electron分支否则降级到WebRTC只拿内网IP。我在实际项目里是把这个判断封装成一个getClientEnv()函数返回electron | browser | ie上游逻辑只认返回值。5.2 WebRTC拿到的是 rtc-xxxx.local不是192.168.x.x现象Chrome 80以上版本跑WebRTC取内网IPcandidate里的地址是.local结尾的混淆名。原因Chromium默认开启enable-webrtc-hide-local-ips-with-mdns出于防DNS rebinding的考虑把本地IP隐藏了。Firefox部分版本没这问题所以同一套代码在不同浏览器结果不一样。解决纯浏览器JS无法解mDNS要么换Electron做采集端要么通过Chrome企业策略给终端关闭该flag。做产品方案时不要承诺“纯浏览器一定拿到内网IP”这句承诺一定会被Chrome升级打破。5.3 拿UA、canvas画的图去“算”MAC属于玄学现象网上一些采集代码把UA、分辨率、canvas指纹、时区拼起来声称能生成MAC。原因浏览器模型暴露的只有指纹特征没有网卡物理地址。指纹可以做概率关联但不可能等于MAC。营销号把两者混为一谈造成了很多假教程。解决做设备识别时把“指纹ID”和“MAC”当成两个字段分开存储。指纹ID只用于会话关联MAC 只在Electron/ActiveX路径下有值不混用。5.4 ActiveX在Edge和Chrome里直接抛异常现象把WMI脚本从IE拷到新页面一行new ActiveXObject就断脚本。原因ActiveX桥只有IE内核才有Edge Chromium已经彻底移除。某些打着“兼容模式”的国产浏览器也是空壳不实现这个接口。解决调用前先做typeof ActiveXObject undefined判断仅在IE分支里加载WMI脚本再包一层try-catch兜底。事件注册用降级写法在IE下用attachEvent标准浏览器用addEventListener。5.5 XFF被客户端随手伪造后端记录全乱现象同一台终端服务端日志里的IP一会儿是北京云IP一会儿是某海外IP实际人在同一层楼。原因前端发送请求时自己写了X-Forwarded-For头后端不加校验直接取第一个值。有的还是nginx直接透传没覆盖客户端的头。解决按第4章的clientIpFromRequest解析同时把落地网关配好让nginx把客户端传入的XFF清掉再写入自己的转发链proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;这样后端拿到的XFF一定由nginx生成链路才可信。6. 验证技巧拿到返回值后先做三件事第一件事把拿到的IP先转成整数做网段校验。WebRTC返回的候选里不止内网IP可能混入APIPA地址或代理泄漏的公网地址必须过滤掉。function ipToInt(ip) { return ip.split(.).reduce((acc, octet) (acc 8) parseInt(octet, 10), 0) 0; } function isPrivateIP(ip) { if (ip 127.0.0.1 || ip localhost) return true; if (/^10\./.test(ip)) return true; if (/^192\.168\./.test(ip)) return true; if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true; return false; } const ips [192.168.1.5, 169.254.10.20, 10.10.0.8]; console.log(ips.filter(isPrivateIP));ipToInt用位移把四个八位段拼成一个32位整数isPrivateIP直接按前缀匹配私有网段。这段逻辑同时回答了“IP地址转换int”的诉求也能用来在资产入库时统一IP的存储格式。第二件事多网卡场景下用MAC做去重和关联。os.networkInterfaces()循环里同一个物理网卡的IPv4/IPv6条目会重复返回同一个MAC必须用Set收口做完掉之后再按MAC的OUI前缀判断是不是虚拟机。我给终端识别模块加过一道“MAC唯一性”校验同一批采集数据里如果出现两个不同hostname但MAC一样说明在虚拟机模板克隆环境里跑这种终端的软件授权记录要单独归档。第三件事跨浏览器对比验证。我每次改完采集逻辑固定用下面这个矩阵自测环境公网IP内网IP真实MAC真实主机名Chrome/Edge可用mDNS混淆不可用不可用Firefox可用部分版本可用不可用不可用IE ActiveX可用可用可用可用Electron可用可用可用可用自测时重点不是看有没有返回值而是看同一配置下两次返回值是否一致内网IP变了说明终端换了网段MAC变了说明可能换了USB网卡或虚拟机重置主机名变了基本就是系统重装过。从那以后我每次做这类采集逻辑都要强制走一遍这套验证流程先过滤、再去重、最后做跨浏览器比对数据进库前把脏值清干净再落表。希望帮到你。本文还有配套的精品资源点击获取