150 lines
3.8 KiB
Markdown
150 lines
3.8 KiB
Markdown
---
|
||
version: 1.1
|
||
last-updated: 2026-03-05
|
||
status: implemented
|
||
---
|
||
|
||
# Device ID 生成方案
|
||
|
||
## 概述
|
||
|
||
为 Electron 客户端生成稳定、安全的设备唯一标识符(deviceId),用于设备识别、授权绑定等场景。
|
||
|
||
---
|
||
|
||
## 方案
|
||
|
||
### 核心公式
|
||
|
||
```
|
||
deviceId = SHA-256(machineId + appSalt)
|
||
```
|
||
|
||
- `machineId`:操作系统级机器标识(通过 `node-machine-id` 获取原始值)
|
||
- `appSalt`:应用固定盐值,避免不同应用使用相同 machineId 产生碰撞
|
||
|
||
### 实现
|
||
|
||
```typescript
|
||
import { machineIdSync } from "node-machine-id";
|
||
import { createHash } from "crypto";
|
||
import * as os from "os";
|
||
import log from "electron-log";
|
||
|
||
const APP_SALT = "qiming-agent";
|
||
let cachedDeviceId: string | null = null;
|
||
|
||
export function getDeviceId(): string {
|
||
if (cachedDeviceId) return cachedDeviceId;
|
||
|
||
let raw: string;
|
||
try {
|
||
raw = machineIdSync(true);
|
||
} catch (e) {
|
||
log.warn("[DeviceId] Failed to read machineId, using hostname fallback:", e);
|
||
raw = os.hostname();
|
||
}
|
||
|
||
cachedDeviceId = createHash("sha256")
|
||
.update(raw + APP_SALT)
|
||
.digest("hex");
|
||
|
||
log.info(`[DeviceId] ${cachedDeviceId}`);
|
||
return cachedDeviceId;
|
||
}
|
||
```
|
||
|
||
### 各平台 machineId 数据源
|
||
|
||
| 平台 | 数据源 | 说明 |
|
||
|------|--------|------|
|
||
| **macOS** | `ioreg` → `IOPlatformUUID` | 硬件 UUID,无需管理员权限 |
|
||
| **Windows** | 注册表 `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` | 系统安装时生成 |
|
||
| **Linux** | `/var/lib/dbus/machine-id` | systemd 机器 ID |
|
||
|
||
### 依赖
|
||
|
||
| 包名 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| `node-machine-id` | ^1.1.12 | 跨平台机器 ID 读取,零 native 依赖 |
|
||
|
||
`crypto` 为 Node.js 内置模块,无需额外安装。
|
||
|
||
---
|
||
|
||
## 设计要点
|
||
|
||
### 1. 稳定性
|
||
|
||
- 同一台机器每次调用返回相同值
|
||
- 进程内缓存,避免重复读取系统命令
|
||
|
||
### 2. 安全性
|
||
|
||
- SHA-256 单向 hash,不暴露原始 machineId
|
||
- 加入应用盐值,防止跨应用关联
|
||
|
||
### 3. 唯一性
|
||
|
||
- machineId 由操作系统保证唯一
|
||
- 加盐 hash 后不同应用产生不同 deviceId
|
||
|
||
### 4. 位置
|
||
|
||
- 实现在 **main process**(`src/main/services/` 下)
|
||
- renderer 通过 IPC + preload bridge 获取
|
||
|
||
---
|
||
|
||
## 架构集成
|
||
|
||
```
|
||
Main Process
|
||
├── src/main/services/system/deviceId.ts # 实现 + 缓存 + 日志
|
||
├── src/main/services/system/index.ts # re-export
|
||
├── src/main/main.ts # 启动时调用并输出日志
|
||
└── src/main/ipc/appHandlers.ts # IPC handler
|
||
|
||
Preload
|
||
└── src/preload/index.ts # contextBridge 暴露
|
||
|
||
Renderer
|
||
└── window.electronAPI.app.getDeviceId() # 调用
|
||
```
|
||
|
||
### IPC 通道
|
||
|
||
```typescript
|
||
// main (appHandlers.ts)
|
||
ipcMain.handle("app:getDeviceId", () => getDeviceId());
|
||
|
||
// preload (index.ts)
|
||
app: {
|
||
getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"),
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 特性对比(为何选此方案)
|
||
|
||
| 方案 | 稳定性 | 安全性 | 依赖 | 备注 |
|
||
|------|--------|--------|------|------|
|
||
| **hash(machineId + salt)** | 高 | 高 | 1 个 npm 包 | **选用** |
|
||
| 纯 machineId | 高 | 低(暴露原始值) | 1 个 npm 包 | 不推荐 |
|
||
| hw-fingerprint(硬件指纹) | 中(换硬件会变) | 高 | 重依赖 systeminformation | 过重 |
|
||
| 本地持久化 UUID | 低(删文件会变) | 中 | 无 | 不绑定设备 |
|
||
| hash(machineId + randomUUID) | 无(每次不同) | - | - | 错误方案 |
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
1. **容器/镜像环境**:基于镜像的环境可能共享相同 machine-id,Linux 下可用 `dbus-uuidgen` 重新生成
|
||
2. **Windows 系统更新**:MachineGuid 在重大系统更新后极少数情况下可能变化
|
||
3. **macOS 权限**:IOPlatformUUID 无需额外权限,区别于 IOPlatformSerialNumber(新版 macOS 受限)
|
||
|
||
---
|
||
|
||
*参考: 此方案为 SaaS 客户端常用的设备标识模式*
|