这份文档主要整理我如何在 Electron 项目中通过 License 绑定当前设备的 MAC 地址,实现设备限制、受诺授权管理。
之前在Electron项目中添加了 License 激活系统集成功能(可参考之前发布的Electron中添加 License 激活系统集开发步骤),现在新增需求,需要将license和设备MAC绑定。
需求背景:
用户输入license需要和设备MAC绑定,同一个设备只能安装一次,也就是(一机一码)
流程图:
完整的 License 激活流程图,涵盖读取、解析、验证签名与设备绑定等关键步骤

绑定 MAC 的概念:
License 中包含设备唯一标识符,通常为网卡 MAC 地址,应用启动时校验 MAC 是否与授权信息一致,若不符则禁止进入。
一、准备工作
安装了systeminformation:
npm install systeminformation
准备创建以下文件:
device-id.ts
:用于获取设备 MAC。license.js
:用于生成绑定 MAC 的 license。license-check.ts
:用于校验 license 合法性。
二、创建device-id.ts(主进程和 license.js 通用)
device-id.ts最好和主进程main.ts同层级
javascript
import si from 'systeminformation'
// systeminformation 是一个用于获取当前设备硬件信息的 Node.js 库,
// 可以跨平台运行(Windows/macOS/Linux),使用npm install systeminformation安装
/**
* 正式环境使用
* 获取当前设备的唯一标识(MAC 地址)
* 用于绑定 License 到当前机器,防止跨设备拷贝使用
*/
export async function getDeviceId(): Promise<string> {
const interfaces = await si.networkInterfaces()// 获取所有网络接口信息(包含 MAC 地址、是否虚拟、是否内网等)
const iface = interfaces.find(i =>
!i.virtual && //不是虚拟网卡(VirtualBox、VMware 等虚拟环境)
i.mac && //有 MAC 地址
i.mac !== "00:00:00:00:00:00" && //不是默认空地址
!i.iface.toLowerCase().includes("vmware") && // 忽略虚拟网卡
!i.iface.toLowerCase().includes("loopback")
)// 从中找到第一个真实的、非虚拟的、有有效 MAC 地址的接
return iface?.mac.toLowerCase() || "UNKNOWN"// 返回找到的 MAC 地址,如果找不到则返回 UNKNOWN
}
//!!!!!!!!!!!!注意:伪造设备环境测试(开发调试用:用于只有一台电脑时模拟测试临时,写死一个假的 MAC 地址:
// export async function getDeviceId(): Promise<string> {
// return "aa:bb:cc:dd:ee:ff"; // 假的设备
// }
三、创建 license.js(开发者执行)
生成 license.js 文件:
javascript
const fs = require('fs')
const crypto = require('crypto')
const si = require('systeminformation') // 推荐用这个systeminformation库,和主进程保持一致
const secret = 'autocontrol-jidian'; //HMAC 签名用的密钥(保持私密,不能泄露)
// 获取当前设备主 MAC 地址(忽略虚拟网卡等)
async function getDeviceId() {
const interfaces = await si.networkInterfaces()
const iface = interfaces.find(
(i) =>
!i.virtual &&
i.mac &&
i.mac !== '00:00:00:00:00:00' &&
!i.iface.toLowerCase().includes('vmware') &&
!i.iface.toLowerCase().includes('loopback')
)
return iface?.mac.toLowerCase() || 'UNKNOWN'
}
// 主逻辑:生成 license token
(async () => {
const deviceId = await getDeviceId() //获取设备
// License 的有效载荷内容
const payload = {
userId: 'Leaff19950212_&#', // 用户标识(可自定义)
expire: '2025-12-31', // 设置过期日期
deviceId: deviceId, // 当前设备的 MAC 地址(用于绑定设备)
}
const raw = JSON.stringify(payload) //序列化 payload 内容为字符串
const signature = crypto
.createHmac('sha256', secret)
.update(raw)
.digest('hex') //对 payload 进行 HMAC-SHA256 签名
const token = Buffer.from(raw).toString('base64') + '.' + signature //最终 License 格式:Base64(payload).signature
console.log('License Token:\n', token)
console.log('deviceId (MAC):', payload.deviceId)
// 将 token 写入文件(软件安装时复制进去)
fs.writeFileSync('license.dat', token)
})()
四、创建license-check.ts
typescript
// license-check.ts 要和mian.ts 在同一目录下,否则无法被构建
import * as crypto from "crypto";
const secretKey = "autocontrol-jidian";
export interface LicenseResult {
valid: boolean;
reason?: string;
payload?: {
userId: string;
expire: string;
deviceId: string;//获取设备
};
}
export function checkLicense(token: string): LicenseResult {
try {
const [encodedPayload, signature] = token.split(".");
const rawPayload = Buffer.from(encodedPayload, "base64").toString("utf-8");
const expectedSig = crypto.createHmac("sha256", secretKey).update(rawPayload).digest("hex");
if (expectedSig !== signature) {
return { valid: false, reason: "签名不一致" };
}
const payload = JSON.parse(rawPayload);
const now = new Date();
const expire = new Date(payload.expire);
if (now > expire) {
return { valid: false, reason: `License 已于 ${payload.expire} 过期`, payload };
}
return { valid: true, payload };
} catch (err) {
return { valid: false, reason: "License 格式错误" };
}
}
五、主进程校验 MAC 是否一致
在主进程 main.ts
中使用如下逻辑进行绑定设备校验:用 getDeviceId()
获取当前 MAC,并与 payload 中 deviceId 对比:
在主进程 main.ts
的app.whenReady()中加入以下关键代码
javascript
import { getDeviceId } from "./device-id";//引入设备ID
// deviceId 有可能有大小写差异(应统一处理)MAC 地址有时表现为:f7:8c:9d:c9:c6:f9 或 F7:8C:9D:C9:C6:F9
if (result.payload?.deviceId.toLowerCase() !== currentDeviceId.toLowerCase()) {
console.log("License 中绑定的是:", result.payload?.deviceId);
dialog.showErrorBox("激活失败", "License 不属于当前设备");
app.quit();
return;
}
主进程 main.ts完整代码示例如下:
typescript
import { app, BrowserWindow, ipcMain, session, dialog } from "electron"
import { join } from "path"
const Store = require("electron-store")
const path = require("path")
import { getDeviceId } from "./device-id";//引入设备ID
const prompt = require('electron-prompt');//引入弹窗组件
import { checkLicense, LicenseResult } from './license-check';//引入许可证验证函数
let licensePayload: { userId: string; expire: string } | null = null;
const LICENSE_FILE = path.join(app.getPath('userData'), 'license.dat');
const sequelize = require("../../config/database") // 引入数据库实例
// 其他逻辑。。。。。。
app.whenReady().then(async() => {
// 集成了 license 检查 + 弹窗逻辑
const licensePath = path.join(app.getPath("userData"), "license.dat");
const currentDeviceId = await getDeviceId();// 获取当前设备的 MAC 地址
console.log("当前设备 MAC:", currentDeviceId);
fs.writeFileSync("current-device-id.log", currentDeviceId);
// 未激活(无 license 文件)判断 license 文件是否存在
if (!fs.existsSync(licensePath)) {
const inputToken = await prompt({
title: "激活软件",
label: "请输入 License:",
inputAttrs: { type: "text" },
type: "input",
});
if (!inputToken) {
dialog.showErrorBox("未授权", "必须输入 License,程序即将退出。");
app.quit();
return;
}
// 激活成功时保存 payload 激活成功时保存 payload
const result: LicenseResult = checkLicense(inputToken.trim());
if (!result.valid) {
dialog.showErrorBox("激活失败", result.reason || "License 无效")
app.quit()
return
}
// deviceId 有可能有大小写差异(应统一处理)MAC 地址有时表现为:f7:8c:9d:c9:c6:f9 或 F7:8C:9D:C9:C6:F9
if (result.payload?.deviceId.toLowerCase() !== currentDeviceId.toLowerCase()) {
console.log("License 中绑定的是:", result.payload?.deviceId);
dialog.showErrorBox("激活失败", "License 不属于当前设备");
app.quit();
return;
}
// 激活成功,保存到文件
licensePayload = result.payload!;
fs.writeFileSync(licensePath, inputToken.trim());
} else {
// 已激活,读取并校验 license 文件
const savedToken = fs.readFileSync(licensePath, "utf-8").trim();
const result = checkLicense(savedToken);
if(!result.valid) {
dialog.showErrorBox("License 无效", result.reason || "本地 License 被篡改或已过期")
fs.unlinkSync(licensePath)
app.quit()
return
}
if (result.payload?.deviceId !== currentDeviceId) {
dialog.showErrorBox("License 无效", "License 不属于当前设备")
fs.unlinkSync(licensePath)
app.quit()
return
}
licensePayload = result.payload!
}
// 通过校验后,启动主窗口和其他逻辑
createWindow()
// 同步数据库
createDatabase()
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": ["script-src 'self' 'unsafe-eval' blob:; worker-src 'self' blob:"],
},
})
})
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
六、如何测试
想测试「设备不一致时无法激活」的逻辑 ,这其实就是验证的 License 是否真正绑定了设备(MAC 地址)
方法一:在另一台电脑上测试
推荐方式
步骤:
- 在 电脑 A 上运行
node license.js
拿到合法的 license token(绑定了 A 的 MAC) - 把
license.dat
或 token 字符串复制到 电脑 B - 在电脑 B 启动你的 Electron 软件,输入这个 token
✅ 预期结果:

这说明设备校验成功阻止了在非授权机器上使用。
方法二:伪造设备环境测试(开发调试用)
⚠️ ****用于你只有一台电脑时模拟测试
方法:
在 device-id.ts
的 getDeviceId()
中,临时写死一个假的 MAC 地址:
javascript
export async function getDeviceId(): Promise<string> {
return "aa:bb:cc:dd:ee:ff"; // 假的设备
}
或者模拟多个网卡,优先取另一个接口。
然后重新启动软件,用原来的 license(绑定 比如:f7:8c:8d:c8:c5:f8
)尝试激活。
✅ 预期:

说明绑定确实生效了。