Electron中将 License 和设备 MAC 地址绑定流程

这份文档主要整理我如何在 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 地址)

方法一:在另一台电脑上测试

推荐方式

步骤:

  1. 在 电脑 A 上运行 node license.js
    拿到合法的 license token(绑定了 A 的 MAC)
  2. license.dat 或 token 字符串复制到 电脑 B
  3. 在电脑 B 启动你的 Electron 软件,输入这个 token

✅ 预期结果:

这说明设备校验成功阻止了在非授权机器上使用。


方法二:伪造设备环境测试(开发调试用)

⚠️ ****用于你只有一台电脑时模拟测试

方法:

device-id.tsgetDeviceId() 中,临时写死一个假的 MAC 地址:

javascript 复制代码
export async function getDeviceId(): Promise<string> {
  return "aa:bb:cc:dd:ee:ff"; // 假的设备
}

或者模拟多个网卡,优先取另一个接口。

然后重新启动软件,用原来的 license(绑定 比如:f7:8c:8d:c8:c5:f8)尝试激活。

✅ 预期:

说明绑定确实生效了。

相关推荐
好了来看下一题2 小时前
使用 React+Vite+Electron 搭建桌面应用
前端·react.js·electron
rookie fish7 小时前
如何控制electron的应用在指定的分屏上打开[特殊字符]
前端·javascript·electron
hweiyu007 小时前
Electron简介(附电子书学习资料)
前端·javascript·electron
张童瑶2 天前
Vue Electron 使用来给若依系统打包成exe程序,出现登录成功但是不跳转页面(已解决)
javascript·vue.js·electron
依了个旧2 天前
Electron内嵌网页实现打印预览功能
electron
朝阳393 天前
Electron-vite【实战】MD 编辑器 -- 编辑区(含工具条、自定义右键快捷菜单、快捷键编辑、拖拽打开文件等)
javascript·electron·编辑器
朝阳393 天前
Electron-vite【实战】MD 编辑器 -- 大纲区(含自动生成大纲,大纲缩进,折叠大纲,滚动同步高亮大纲,点击大纲滚动等)
javascript·electron·编辑器
持久的棒棒君4 天前
npm安装electron下载太慢,导致报错
前端·electron·npm
LEAFF4 天前
Electron License 激活系统集成开发步骤
vue.js·typescript·electron