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.ts 的 getDeviceId() 中,临时写死一个假的 MAC 地址:

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

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

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

✅ 预期:

说明绑定确实生效了。

相关推荐
月走乂山1 天前
DSH 桌面版用 DeepSeek 报 REQUEST_EXTENSION 的根因定位与修复
electron·node.js·插件系统·deepseek·bug排查·dsh·deepseek harness
mont evergreen2 天前
一封从外部世界寄来的信:二维码数据如何穿越三重门,抵达你的屏幕
electron·vue·事件
fellow993 天前
DSH Desktop:把 DeepSeek Harness 变成真正的桌面应用
electron·agent·ai编程·deepseek
CDwenhuohuo3 天前
electron pc项目打包成桌面端
前端·javascript·electron
不可能片场5 天前
命令行中文变问号 我用环境变量救了场
前端·electron
不可能片场5 天前
AI视频口型对齐 用音频驱动说话镜头
前端·electron
Zoom5 天前
【开源】7 天,我用 Rust 复刻了 FinalShell!Rhost v1.0.0 发布
rust·electron·shell
爱丶不疚7 天前
Electron: Sentry 都采集了什么?你知道吗
前端·electron
格数致用7 天前
03- Luckysheet与Vue3+Electron集成实战指南
electron·vue3·luckysheet