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 显示到期日期与剩余天数
相关推荐
Eason_Lou12 分钟前
vue flow使用注意事项
前端·vue.js
moMo2 小时前
深入 NestJS:从工厂模式到装饰器模式的架构之美
typescript·nestjs
马可家的菠萝5 小时前
Vue3 + Canvas 手绘笔记工程化实践:别把画布只当成一张 PNG
前端·vue.js·算法
用户841794814565 小时前
如何用 vue 甘特图来实现计划和实际双任务条进度展示
vue.js
卸任5 小时前
AI英语学习助手:从翻译工具到 AI 英语学习助手
前端·electron
京东云开发者8 小时前
【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手【最终版】
前端·vue.js·音视频开发
一颗烂土豆9 小时前
ECharts 太平面?试试这款 Vue 3D 图表库
前端·vue.js·echarts
烬羽9 小时前
NestJS 依赖注入:Controller 里那个没有 new 的 service,到底从哪来的?
设计模式·typescript·nestjs
渣波9 小时前
NestJS 企业级后端架构实战:从核心代码到工程化思维的深度重构
前端·typescript·nestjs
宿6749 小时前
vscode文件夹
前端·vue.js·vscode