Electron License 激活系统集成开发步骤

需求背景

①通过node license.js获取license,发给用户;

②Electron打包安装后,打开软件需要先弹窗,需要用户输入我们发给他们的license,license匹配正确才能使用软件。否则提示无效或不匹配;

功能特性

  • 首次运行需输入 License 才可使用
  • License 格式包含签名与过期时间
  • 本地缓存激活信息(license.dat)
  • 启动时自动校验 License 是否有效及是否过期
  • Vue 界面右上角实时展示 License 到期时间

License Token 格式:

javascript 复制代码
Base64(JSON payload) + '.' + HMAC-SHA256 签名

示例 payload:

json 复制代码
{
  "userId": "LiuZhe19981204_&#",
  "expire": "2025-12-31"
}

一、 安装一个弹窗组件

复制代码
npm install electron-prompt

二、根目录 创建 license.js

ini 复制代码
const fs = require('fs');
const crypto = require('crypto');

const secret = 'autocontrol-jidian';
const payload = {
  userId: 'Leaf19950212_&#',
  expire: '2025-12-31' // 设置过期日期
};

const raw = JSON.stringify(payload);
const signature  = crypto.createHmac('sha256', secret).update(raw).digest('hex');
const token = Buffer.from(raw).toString('base64') + '.' + signature;

console.log("token", token)
fs.writeFileSync('license.dat', token);

三、添加license校验方法, 创建 license-check.ts

license-check.ts 要和mian.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;
  };
}

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 格式错误" };
  }
}

四、main.ts引入license-check.ts

引入,并在app.whenReady().then中加入以下【集成 license 检查 + 弹窗逻辑】:

typescript 复制代码
const prompt = require('electron-prompt');//引入弹窗组件
import { checkLicense, LicenseResult  } from './license-check';//引入许可证验证函数
let licensePayload: { userId: string; expire: string } | null = null;

app.whenReady().then(async() => {
// -------------------start:集成了 license 检查 + 弹窗逻辑------------------
const licensePath = path.join(app.getPath("userData"), "license.dat");

  // 判断 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) {
      licensePayload = result.payload!;
      fs.writeFileSync(licensePath, inputToken.trim());
    } else {
      dialog.showErrorBox("激活失败", result.reason || "License 无效,请联系管理员。");
      app.quit();
      return;
    }

    // 写入本地 license 文件
    fs.writeFileSync(licensePath, inputToken.trim());
  } else {
    const savedToken = fs.readFileSync(licensePath, "utf-8").trim();
    const result = checkLicense(savedToken);
    if (result.valid) {
      licensePayload = result.payload!;
    } else {
      dialog.showErrorBox("License 无效", result.reason || "本地 License 被篡改或过期,程序将退出。");
      fs.unlinkSync(licensePath);
      app.quit();
      return;
    }
  }
// -------------------end:集成了 license 检查 + 弹窗逻辑------------------

// -------------------start:通过校验后,启动主窗口和其他逻辑------------------
// 启动主窗口和其他逻辑
  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", () => {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
// -------------------end:集成了 license 检查 + 弹窗逻辑------------------
})

五、在preload.ts暴露获取license相关的方法

便于前端查询和显示license过期时间

arduino 复制代码
contextBridge.exposeInMainWorld("electronAPI", {
  //.....
  // license相关
  getLicenseInfo: () => ipcRenderer.invoke("get-license-info"),
}

六、前端页面显示license过期

xml 复制代码
<!-- 动态显示 License 有效期 -->
<template>
<div class="license-banner" v-if="licenseExpire">
   License 有效至:{{licenseExpire }}
</div>
</template>
  
<script setup>
import { ref, reactive, onMounted } from 'vue'
const licenseExpire = ref("");
onMounted(() => {
  // 确保在 preload 中contextBridge.exposeInMainWorld暴露了 Electron API,并且添加了getLicenseInfo方法
  window.electronAPI.getLicenseInfo().then((info) => {
    console.log("License Info:", info);
    if (info?.expire) {
      licenseExpire.value = info.expire;
    }
  });
});
</script>

<style scoped>
/* license 样式 */
.license-banner {
  position: absolute;
  top: 10px;
  right: 20px;
  font-size: 12px;
  color: #888;
}
</style>

七、测试用例

场景 预期行为
没有 license.dat 弹窗要求输入
输入正确 license 软件启动并保存激活状态
第二次启动 自动读取,无需输入
修改 license 提示签名不一致,退出程序
license 过期 提示已过期,退出程序
页面 UI 显示到期日期与剩余天数
相关推荐
十年砍柴---小火苗17 分钟前
vue的created和mounted区别
javascript·vue.js·ecmascript
代码密语者18 分钟前
《彻底搞懂 TypeScript 类型导入:export、declare 与全局类型的正确打开方式》
typescript
bitbitDown22 分钟前
同事用了个@vue:mounted,我去官网找了半天没找到
前端·javascript·vue.js
仰望.1 小时前
vxe-table vue 表格复选框多选数据,实现快捷键 Shift 批量选择功能
vue.js·vxe-table
武昌库里写JAVA1 小时前
iview组件库:关于分页组件的使用与注意点
java·vue.js·spring boot·学习·课程设计
xihehua1 小时前
Nuxt Vue3
vue.js
步行cgn1 小时前
v-bind 与 v-model 的区别与联系详解
前端·vue.js
non_hana3 小时前
一些 linter & formatter 配置最佳实践
typescript·node.js·eslint
Data_Adventure3 小时前
如何在本地测试自己开发的 npm 包
前端·vue.js·svg
萌萌哒草头将军4 小时前
⚓️ Oxlint 1.0 版本发布,比 ESLint 快50 到 100 倍!🚀🚀🚀
前端·javascript·vue.js