import os import sys import subprocess import uuid import hashlib import platform import logging import re import json from pathlib import Path from typing import Dict, Optional, List, Tuple # 获取日志记录器 logger = logging.getLogger(__name__) class DeviceFingerprint: """设备指纹收集器 - 用于生成唯一的设备标识""" def __init__(self): """初始化设备指纹收集器""" self.system = platform.system() self.fingerprint_cache_file = (Path(__file__).parent.parent.parent / "config" / ".device_fingerprint") def get_hostname(self) -> str: """获取计算机主机名""" return platform.node() def get_mac_address(self) -> str: """获取MAC地址(小写格式)""" mac = uuid.UUID(int=uuid.getnode()).hex[-12:] return ":".join([mac[i:i + 2] for i in range(0, 12, 2)]).lower() def get_all_mac_addresses(self) -> List[Dict[str, str]]: """获取所有网络适配器的MAC地址""" mac_addresses = [] try: if self.system == "Windows": # Windows系统通过WMI获取所有网络适配器 import wmi w = wmi.WMI() for nic in w.Win32_NetworkAdapter(): if nic.MACAddress: # 确保MAC地址为小写 mac_addr = nic.MACAddress.lower() if nic.MACAddress else "" mac_addresses.append({ "name": nic.Name, "mac": mac_addr, "device_id": nic.DeviceID, "adapter_type": getattr(nic, "AdapterType", ""), "net_connection_id": getattr(nic, "NetConnectionID", ""), "physical": nic.PhysicalAdapter if hasattr(nic, "PhysicalAdapter") else False }) # 其他系统的实现可以在这里添加 except Exception as e: logger.error(f"获取所有MAC地址时出错: {e}") return mac_addresses def get_primary_mac_address(self) -> Optional[Tuple[str, str]]: """ 智能选择最适合的物理网卡MAC地址 Returns: Tuple[str, str]: (MAC地址, 网卡类型描述) """ all_macs = self.get_all_mac_addresses() # 如果没有找到任何MAC地址,返回None if not all_macs: return None # 对适配器进行分类 ethernet_adapters = [] # 有线网卡 wifi_adapters = [] # WiFi网卡 bluetooth_adapters = [] # 蓝牙适配器 physical_adapters = [] # 其他物理网卡 virtual_adapters = [] # 虚拟网卡 for adapter in all_macs: # 确保name和connection_id不是None name = str(adapter.get("name", "")).lower() # 不使用connection_id进行分类,避免None错误 physical = adapter.get("physical", False) # 主要通过名称来判断网卡类型 if (any(keyword in name for keyword in ["ethernet", "realtek", "intel", "broadcom"]) and physical): ethernet_adapters.append(adapter) elif (any(keyword in name for keyword in ["wi-fi", "wifi", "wireless", "wlan"]) and physical): wifi_adapters.append(adapter) elif "bluetooth" in name: bluetooth_adapters.append(adapter) elif physical: physical_adapters.append(adapter) else: virtual_adapters.append(adapter) # 按优先级选择MAC地址 if ethernet_adapters: return ethernet_adapters[0]["mac"], "有线网卡" elif wifi_adapters: return wifi_adapters[0]["mac"], "WiFi网卡" elif bluetooth_adapters: return bluetooth_adapters[0]["mac"], "蓝牙适配器" elif physical_adapters: return physical_adapters[0]["mac"], "物理网卡" elif virtual_adapters: return virtual_adapters[0]["mac"], "虚拟网卡" # 如果所有分类都为空,返回第一个MAC地址作为备选 return all_macs[0]["mac"], "未知类型" def get_bluetooth_mac_address(self) -> Optional[str]: """获取蓝牙适配器的MAC地址""" try: all_macs = self.get_all_mac_addresses() for mac_info in all_macs: # 检查名称中是否包含"Bluetooth"关键字 if "Bluetooth" in mac_info.get("name", ""): return mac_info.get("mac") return None except Exception as e: logger.error(f"获取蓝牙MAC地址时出错: {e}") return None def get_cpu_info(self) -> Dict: """获取CPU信息""" cpu_info = { "processor": platform.processor(), "machine": platform.machine() } try: if self.system == "Windows": # Windows系统通过WMI获取CPU信息 import wmi w = wmi.WMI() processor = w.Win32_Processor()[0] cpu_info["id"] = processor.ProcessorId.strip() cpu_info["name"] = processor.Name.strip() cpu_info["cores"] = processor.NumberOfCores elif self.system == "Linux": # Linux系统通过/proc/cpuinfo获取CPU信息 with open('/proc/cpuinfo', 'r') as f: info = f.readlines() cpu_id = None model_name = None cpu_cores = 0 for line in info: if "serial" in line or "Serial" in line: # 一些Linux系统可能会有CPU序列号 cpu_id = line.split(':')[1].strip() elif "model name" in line: model_name = line.split(':')[1].strip() elif "cpu cores" in line: cpu_cores = int(line.split(':')[1].strip()) if cpu_id: cpu_info["id"] = cpu_id if model_name: cpu_info["name"] = model_name if cpu_cores: cpu_info["cores"] = cpu_cores elif self.system == "Darwin": # macOS # macOS通过系统命令获取CPU信息 cmd = "sysctl -n machdep.cpu.brand_string" model_name = subprocess.check_output(cmd, shell=True).decode().strip() cmd = "sysctl -n hw.physicalcpu" cpu_cores = int(subprocess.check_output(cmd, shell=True).decode().strip()) # macOS没有直接暴露CPU ID,使用其他信息组合 cmd = "ioreg -l | grep IOPlatformUUID" try: platform_uuid = subprocess.check_output(cmd, shell=True).decode().strip() uuid_match = re.search(r'IOPlatformUUID" = "([^"]+)"', platform_uuid) if uuid_match: cpu_info["id"] = uuid_match.group(1) except Exception: pass cpu_info["name"] = model_name cpu_info["cores"] = cpu_cores except Exception as e: logger.error(f"获取CPU信息时出错: {e}") return cpu_info def get_disk_info(self) -> List[Dict]: """获取硬盘信息""" disks = [] try: if self.system == "Windows": # Windows系统通过WMI获取硬盘信息 import wmi w = wmi.WMI() for disk in w.Win32_DiskDrive(): if disk.SerialNumber: disks.append({ "model": disk.Model.strip(), "serial": disk.SerialNumber.strip(), "size": str(int(disk.Size or 0)) }) elif self.system == "Linux": # Linux系统通过lsblk命令获取硬盘信息 cmd = "lsblk -d -o NAME,SERIAL,MODEL,SIZE --json" try: result = subprocess.check_output(cmd, shell=True).decode() data = json.loads(result) for device in data.get("blockdevices", []): if device.get("serial"): disks.append({ "model": device.get("model", "").strip(), "serial": device.get("serial").strip(), "size": device.get("size", "").strip() }) except Exception: # 备用方法通过/dev/disk/by-id获取 try: disk_ids = os.listdir('/dev/disk/by-id') for disk_id in disk_ids: if (not disk_id.startswith('usb-') and not disk_id.startswith('nvme-eui') and not disk_id.startswith('wwn-') and 'part' not in disk_id): disks.append({ "model": "Unknown", "serial": disk_id, "size": "0" }) except Exception: pass elif self.system == "Darwin": # macOS # macOS通过diskutil命令获取硬盘信息 cmd = "diskutil list -plist" try: import plistlib result = subprocess.check_output(cmd, shell=True) data = plistlib.loads(result) for disk in data.get('AllDisksAndPartitions', []): disk_id = disk.get('DeviceIdentifier') if not disk_id: continue # 获取该磁盘的详细信息 cmd = f"diskutil info -plist {disk_id}" disk_info = subprocess.check_output(cmd, shell=True) disk_data = plistlib.loads(disk_info) serial = (disk_data.get('IORegistryEntryName') or disk_data.get('MediaName')) if serial: disks.append({ "model": disk_data.get('MediaName', 'Unknown'), "serial": serial, "size": str(disk_data.get('TotalSize', 0)) }) except Exception as e: logger.error(f"在macOS上获取磁盘信息时出错: {e}") except Exception as e: logger.error(f"获取硬盘信息时出错: {e}") return disks def get_motherboard_info(self) -> Dict: """获取主板信息""" mb_info = {} try: if self.system == "Windows": # Windows通过WMI获取主板信息 import wmi w = wmi.WMI() for board in w.Win32_BaseBoard(): mb_info["manufacturer"] = board.Manufacturer.strip() if board.Manufacturer else "" mb_info["model"] = board.Product.strip() if board.Product else "" mb_info["serial"] = board.SerialNumber.strip() if board.SerialNumber else "" break # 如果没有获取到序列号,尝试使用BIOS序列号 if not mb_info.get("serial"): for bios in w.Win32_BIOS(): if bios.SerialNumber: mb_info["bios_serial"] = bios.SerialNumber.strip() break elif self.system == "Linux": # Linux通过dmidecode命令获取主板信息 try: cmd = "dmidecode -t 2" result = subprocess.check_output( cmd, shell=True, stderr=subprocess.PIPE).decode() for line in result.split('\n'): if "Manufacturer" in line: mb_info["manufacturer"] = line.split(':')[1].strip() elif "Product Name" in line: mb_info["model"] = line.split(':')[1].strip() elif "Serial Number" in line: mb_info["serial"] = line.split(':')[1].strip() except Exception: # 备用方法从/sys/class/dmi/id获取 try: with open('/sys/class/dmi/id/board_vendor', 'r') as f: mb_info["manufacturer"] = f.read().strip() with open('/sys/class/dmi/id/board_name', 'r') as f: mb_info["model"] = f.read().strip() with open('/sys/class/dmi/id/board_serial', 'r') as f: mb_info["serial"] = f.read().strip() except Exception: pass elif self.system == "Darwin": # macOS # macOS使用ioreg命令获取主板或系统信息 try: cmd = "ioreg -l | grep -E '(board-id|IOPlatformSerialNumber|IOPlatformUUID)'" result = subprocess.check_output(cmd, shell=True).decode() board_id = re.search(r'board-id" = <"([^"]+)">', result) if board_id: mb_info["model"] = board_id.group(1) serial = re.search(r'IOPlatformSerialNumber" = "([^"]+)"', result) if serial: mb_info["serial"] = serial.group(1) uuid_match = re.search(r'IOPlatformUUID" = "([^"]+)"', result) if uuid_match: mb_info["uuid"] = uuid_match.group(1) except Exception as e: logger.error(f"在macOS上获取主板信息时出错: {e}") except Exception as e: logger.error(f"获取主板信息时出错: {e}") return mb_info def generate_fingerprint(self) -> Dict: """生成完整的设备指纹""" # 检查是否有缓存的指纹 cached_fingerprint = self._load_cached_fingerprint() if cached_fingerprint: return cached_fingerprint # 获取主要网卡MAC地址 primary_mac, mac_type = self.get_primary_mac_address() or (self.get_mac_address(), "系统MAC") # 收集各种硬件信息 fingerprint = { "system": self.system, "hostname": self.get_hostname(), "mac_address": self.get_mac_address(), # 保留系统获取的MAC地址用于兼容 "primary_mac": primary_mac, "primary_mac_type": mac_type, "bluetooth_mac": self.get_bluetooth_mac_address(), "cpu": self.get_cpu_info(), "disks": self.get_disk_info(), "motherboard": self.get_motherboard_info() } # 缓存指纹 self._cache_fingerprint(fingerprint) return fingerprint def _load_cached_fingerprint(self) -> Optional[Dict]: """从缓存文件加载指纹""" if not self.fingerprint_cache_file.exists(): return None try: with open(self.fingerprint_cache_file, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: logger.error(f"加载缓存的设备指纹时出错: {e}") return None def _cache_fingerprint(self, fingerprint: Dict): """缓存指纹到文件""" try: # 确保目录存在 self.fingerprint_cache_file.parent.mkdir(parents=True, exist_ok=True) with open(self.fingerprint_cache_file, 'w', encoding='utf-8') as f: json.dump(fingerprint, f, indent=2, ensure_ascii=False) logger.info("设备指纹已缓存到文件") except Exception as e: logger.error(f"缓存设备指纹时出错: {e}") def generate_hardware_hash(self) -> str: """根据硬件信息生成唯一的哈希值""" fingerprint = self.generate_fingerprint() # 提取最不可变的硬件标识符 identifiers = [] # 主机名 hostname = fingerprint.get("hostname") if hostname: identifiers.append(hostname) # CPU ID if fingerprint.get("cpu", {}).get("id"): identifiers.append(fingerprint["cpu"]["id"]) else: identifiers.append(fingerprint.get("cpu", {}).get("name", "unknown_cpu")) # 主板序列号 mb_serial = fingerprint.get("motherboard", {}).get("serial") if mb_serial and mb_serial != "To be filled by O.E.M.": # 排除默认值 identifiers.append(mb_serial) else: mb_uuid = fingerprint.get("motherboard", {}).get("uuid") if mb_uuid: identifiers.append(mb_uuid) # 硬盘序列号(使用第一个非空的硬盘序列号) for disk in fingerprint.get("disks", []): if disk.get("serial") and disk["serial"] != "0000_0000": identifiers.append(disk["serial"]) break # 如果没有收集到足够的硬件信息,使用MAC地址作为备选 if len(identifiers) < 2: identifiers.append(fingerprint.get("mac_address", "")) # 如果有蓝牙MAC地址,也加入 if fingerprint.get("bluetooth_mac"): identifiers.append(fingerprint.get("bluetooth_mac")) # 将所有标识符连接起来并计算哈希值 fingerprint_str = "||".join(identifiers) return hashlib.sha256(fingerprint_str.encode('utf-8')).hexdigest() def generate_serial_number(self) -> Tuple[str, str]: """ 生成设备序列号 Returns: Tuple[str, str]: (序列号, 生成方法说明) """ fingerprint = self.generate_fingerprint() # 获取主机名,用于序列号生成 hostname = fingerprint.get("hostname", "") short_hostname = "".join(c for c in hostname if c.isalnum())[:8] # 优先使用主网卡MAC地址生成序列号 primary_mac = fingerprint.get("primary_mac") primary_mac_type = fingerprint.get("primary_mac_type", "未知网卡") if primary_mac: # 确保MAC地址为小写且没有冒号 mac_clean = primary_mac.lower().replace(":", "") short_hash = hashlib.md5(mac_clean.encode()).hexdigest()[:8].upper() serial_number = f"SN-{short_hash}-{mac_clean}" return serial_number, primary_mac_type # 备选方案: 尝试使用蓝牙MAC地址 bluetooth_mac = fingerprint.get("bluetooth_mac") if bluetooth_mac: # 确保MAC地址为小写且没有冒号 mac_clean = bluetooth_mac.lower().replace(":", "") short_hash = hashlib.md5(mac_clean.encode()).hexdigest()[:8].upper() serial_number = f"SN-{short_hash}-{mac_clean}" return serial_number, "蓝牙MAC地址" # 备选方案: 使用常规MAC地址 mac_address = fingerprint.get("mac_address") if mac_address: # 确保MAC地址为小写且没有冒号 mac_clean = mac_address.lower().replace(":", "") short_hash = hashlib.md5(mac_clean.encode()).hexdigest()[:8].upper() serial_number = f"SN-{short_hash}-{mac_clean}" return serial_number, "系统MAC地址" # 最后方案: 使用硬件哈希 hardware_hash = self.generate_hardware_hash()[:16].upper() serial_number = f"SN-{hardware_hash}" return serial_number, "硬件哈希值" # 单例模式获取设备指纹 _fingerprint_instance = None def get_device_fingerprint() -> DeviceFingerprint: """获取设备指纹实例(单例模式)""" global _fingerprint_instance if _fingerprint_instance is None: _fingerprint_instance = DeviceFingerprint() return _fingerprint_instance