1. 启动 HTTPS 服务
在 Electron 的主进程中,我们可以直接利用 Node.js 原生的 http 和 https 模块来创建服务。区别在于 HTTPS 需要配置 key (私钥) 和 cert (证书)。
代码实现
import { createServer as createHttpServer } from "http";
import { createServer as createHttpsServer } from "https";
import { generateSiteCert } from "./cert-service"; // 下文会提到的证书服务
const startServer = async (config: ServerConfig) => {
const requestHandler = (req, res) => {
// 处理静态文件或 API 代理逻辑
res.end("Hello Secure World!");
};
let server;
if (config.https.enabled) {
// 关键点:动态生成或读取站点证书
// 返回的 key/cert 是 PEM 格式的字符串
const { key, cert } = generateSiteCert(config.https.domain);
server = createHttpsServer({
key,
cert
}, requestHandler);
} else {
server = createHttpServer(requestHandler);
}
server.listen(config.port, () => {
console.log(`Server running at ${config.https.enabled ? 'https' : 'http'}://localhost:${config.port}`);
});
};2. SSL 证书的自动化生成与认证
我们使用 node-forge 库来完成这些操作,它是一个纯 JavaScript 实现的加密库。
2.1 生成根证书 (Root CA)
CA 证书的有效期通常设置得很长(如 10 年),并且需要标记为 cA: true。
import * as forge from "node-forge";
export const generateCA = () => {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
// 设置有效期 10 年
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
const attrs = [
{ name: "commonName", value: `My Local Dev CA - ${Date.now()}` }, // 加上时间戳避免重名冲突
{ name: "organizationName", value: "Local Dev" }
];
cert.setSubject(attrs);
cert.setIssuer(attrs); // 自签名:颁发者等于使用者
cert.setExtensions([
{ name: "basicConstraints", cA: true }, // 关键:标识为 CA
{ name: "keyUsage", keyCertSign: true, digitalSignature: true },
{ name: "subjectKeyIdentifier" }
]);
// 使用私钥签名
cert.sign(keys.privateKey, forge.md.sha256.create());
return {
key: forge.pki.privateKeyToPem(keys.privateKey),
cert: forge.pki.certificateToPem(cert)
};
};2.2 签发站点证书 (Site Certificate)
站点证书必须包含 SAN (Subject Alternative Name) 和 Server Auth (扩展密钥用法),否则现代浏览器(Chrome 58+)会拦截。
export const generateSiteCert = (domain: string, caKeyPem: string, caCertPem: string) => {
const caKey = forge.pki.privateKeyFromPem(caKeyPem);
const caCert = forge.pki.certificateFromPem(caCertPem);
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = Date.now().toString(); // 唯一的序列号
// 有效期 1 年
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
cert.setSubject([{ name: "commonName", value: domain }]);
cert.setIssuer(caCert.subject.attributes); // 颁发者是我们的 CA
cert.setExtensions([
{ name: "basicConstraints", cA: false },
{
name: "keyUsage",
digitalSignature: true,
keyEncipherment: true
},
{
name: "extKeyUsage", // 关键:必须包含服务器验证
serverAuth: true,
clientAuth: true
},
{
name: "subjectAltName", // 关键:SAN 字段
altNames: [
{ type: 2, value: domain }, // DNS: mysite.local
{ type: 7, ip: "127.0.0.1" } // IP: 127.0.0.1
]
},
{
name: "authorityKeyIdentifier", // 建立信任链的关键
keyIdentifier: caCert.generateSubjectKeyIdentifier().data
}
]);
// 使用 CA 的私钥进行签名
cert.sign(caKey, forge.md.sha256.create());
return {
key: forge.pki.privateKeyToPem(keys.privateKey),
cert: forge.pki.certificateToPem(cert)
};
};2.3 信任根证书 (Windows)
生成的证书默认是不受系统信任的。我们需要调用 Windows 的 certutil 工具将我们的 CA 导入到“受信任的根证书颁发机构”存储区。
这需要管理员权限,我们可以通过 PowerShell 的 Start-Process -Verb RunAs 来触发 UAC 提权弹窗。
import { exec } from "child_process";
export const installCertTrust = (caCertPath: string) => {
// certutil -addstore -user Root "path/to/rootCA.pem"
const psCmd = `Start-Process certutil -ArgumentList '-addstore','-user','Root','"${caCertPath}"' -Verb RunAs -Wait`;
return new Promise((resolve, reject) => {
exec(`powershell "${psCmd}"`, (err) => {
if (err) reject(err);
else resolve();
});
});
};3. 修改系统 Hosts 文件
为了支持 mysite.local 这样的自定义域名指向本地,我们需要修改系统的 Hosts 文件。
Windows 的 Hosts 文件位于 C:\Windows\System32\drivers\etc\hosts。直接写入通常会因为权限不足而失败(即使是管理员权限的 Node 进程有时也会受限)。
稳健的解决方案:
- 读取原 Hosts 内容。
- 在内存中修改(添加
127.0.0.1 mysite.local)。 - 写入到一个临时文件。
- 使用提权 PowerShell 将临时文件强制复制覆盖系统 Hosts 文件。
import * as fs from "fs";
import * as path from "path";
import { app } from "electron"; // 获取临时目录路径
const HOSTS_PATH = "C:\\Windows\\System32\\drivers\\etc\\hosts";
export const updateHosts = (domain: string) => {
// 1. 读取并修改内容
let content = fs.readFileSync(HOSTS_PATH, "utf8");
// ... (省略具体的字符串处理逻辑,如去重、追加) ...
const newContent = content + `\r\n127.0.0.1 ${domain}`;
// 2. 写入临时文件
const tempPath = path.join(app.getPath("userData"), `hosts_temp_${Date.now()}`);
fs.writeFileSync(tempPath, newContent);
// 3. 提权覆盖
// Copy-Item "temp" "hosts" -Force
const psCmd = `Start-Process powershell -ArgumentList '-Command "Copy-Item ''${tempPath}'' ''${HOSTS_PATH}'' -Force"' -Verb RunAs -Wait`;
return new Promise((resolve, reject) => {
exec(`powershell "${psCmd}"`, (err) => {
// 清理临时文件
fs.unlinkSync(tempPath);
if (err) reject(err);
else resolve();
});
});
};4. 总结
- Node-Forge 负责构建合规的证书链,骗过浏览器的安全检查。
- Certutil + PowerShell 负责打通操作系统的证书信任。
- Hosts Hack 负责实现自定义域名的本地回环解析。