需求背景
①通过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 | 显示到期日期与剩余天数 |