聊一聊Electron生产环境cookie失效的问题
目录
项目背景
项目运行在Windows系统下,架构为C/S。使用iframe加载第三方链接(所以没办法从第三方链接处理,只能前端这里处理)
技术栈
列出主要的和对项目影响大的技术框架
- vue3
- electron
- vite
- typescript
下面列出我的package.json(没有藏!都在这里)
注意版本哦
"dependencies": {
"axios": "^1.4.0",
"changedpi": "^1.0.4",
"echarts": "^5.4.2",
"electron-updater": "^6.1.1",
"element-plus": "^2.3.6",
"pinia": "^2.1.3",
"vue": "^3.2.25",
"vue-router": "^4.0.15",
"winston": "^3.14.2",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@types/winston": "^2.4.4",
"@vitejs/plugin-vue": "^2.3.3",
"@vueuse/electron": "^8.5.0",
"electron": "^19.0.0",
"electron-builder": "^23.0.3",
"electron-devtools-installer": "^3.2.0",
"rimraf": "^3.0.2",
"typescript": "^4.5.4",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.0",
"vite": "^2.9.9",
"vite-plugin-electron": "^0.4.5",
"vue-tsc": "^0.34.7"
},
问题描述:
开发环境是存储cookie是没问题的,为什么呢?排查后可以发现对于electron加载前端页面的方式不同
以下是相关代码
// 打开网址(加载页面)
let winURL;
if (app.isPackaged) {
// app已经被打包(生产环境)
winURL = args.route
? path.join(__dirname, `../index.html/#${args.route}`)
: path.join(__dirname, "../index.html");
win.loadFile(winURL);
} else {
// app没有被打包(开发环境)
win.webContents.openDevTools();
winURL = args.route
? `http://${process.env["VITE_DEV_SERVER_HOST"]}:${process.env["VITE_DEV_SERVER_PORT"]}${args.route}?winId=${args.id}`
: `http://${process.env["VITE_DEV_SERVER_HOST"]}:${process.env["VITE_DEV_SERVER_PORT"]}?winId=${args.id}`;
win.loadURL(winURL);
}
以下对于两种方式 win.loadFile(winURL)和win.loadURL(winURL)聊一聊
loadURL(url);
通过从网络加载 URL,我这里是http协议打开一个url地址(类似在浏览器输入网址)
loadURL
用途:用于从网络加载 URL。
参数:
url:要加载的 URL 字符串。
options(可选):一个对象,可以包含以下属性:
httpReferrer:HTTP 引用字符串。
userAgent:用户代理字符串。
extraHeaders:额外的 HTTP 请求头。
postData:POST 数据。
baseURLForDataURL:当加载的数据 URL 时使用的基 URL。
loadFile(filePath);
通过file协议打开一个文件目录地址(类似在本地资源管理器双击html页面)
loadFile
用途:用于从本地文件系统加载 HTML 文件。
参数:
filePath:要加载的本地 HTML 文件路径。
search(可选):查询字符串。
hash(可选):哈希片段。
在使用 loadFile 方法加载本地 HTML 文件时,处理 Cookies 的方式与使用 loadURL 加载网络 URL 有所不同。loadFile 不会自动处理 HTTP 请求和响应头中的 Cookies,因为它是从本地文件系统加载内容的,而不是通过网络请求。
解决问题思路
找到了问题,从源头我们应该把file换成url地址方式就好了。开始我也是这么想的。但是打包后改成url地址,必然要起一个sever服务,那么electron是可以使用http网络模块起vue3打包后的dist文件夹静态http服务的。
思路1(loadFile–换–>loadURL)测试失败
以下是相关代码
// serve-local-resources.ts
import * as http from "http";
import * as path from "path";
import * as fs from "fs";
let server: http.Server | undefined;
interface ServerOptions {
port: number;
distDir: string;
}
function startServer({ port, distDir }: ServerOptions) {
if (server) {
console.log("Server is already running.");
return;
}
server = http.createServer((req, res) => {
const filePath = path.join(distDir, req.url === "/" ? "index.html" : req.url || "");
const extname = path.extname(filePath).toLowerCase();
const mimeTypes: { [key: string]: string } = {
".html": "text/html",
".js": "text/javascript",
".css": "text/css",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpg",
".gif": "image/gif",
".wav": "audio/wav",
".mp4": "video/mp4",
".woff": "application/font-woff",
".ttf": "application/font-ttf",
".eot": "application/vnd.ms-fontobject",
".otf": "application/font-otf",
".svg": "image/svg+xml",
};
const contentType = mimeTypes[extname] || "application/octet-stream";
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code === "ENOENT") {
fs.readFile(
path.join(distDir, "404.html"),
(error404, content404) => {
res.writeHead(404, { "Content-Type": "text/html" });
res.end(content404 || "File not found!", "utf-8");
}
);
} else {
res.writeHead(500);
res.end(`Server Error: ${error.code}`);
}
} else {
res.writeHead(200, { "Content-Type": contentType });
res.end(content, "utf-8");
}
});
});
server.listen(port, () => {
console.log(`Server running at http://127.0.0.1:${port}/`);
});
return server;
}
export default startServer;
在创建窗口前启动服务,app关闭后关掉服务
import { app, BrowserWindow } from "electron";
import { Window, serverPort } from "./window";
import path from "path";
import startServer from "./serve-local-resources";
const isDevelopment: boolean = process.env.NODE_ENV !== "production";
let server: ReturnType<typeof startServer>;
// 创建主窗口
async function createWindow() {
// 启动HTTP服务器
const port = serverPort;
const distDir = path.join(__dirname, "..", "..", "dist");
server = isDevelopment ? void 0 : startServer({ port, distDir });
const window = new Window();
window.listen();
window.createWindows({ isMainWin: true });
window.createTray();
}
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
});
// app.on("ready", createWindow);
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
// 关闭服务器
app.on("will-quit", () => {
if (server) {
server.close(() => {
console.log("HTTP server closed.");
});
}
});
app.on("ready", async () => {
createWindow();
});
if (isDevelopment) {
if (process.platform === "win32") {
process.on("message", (data) => {
if (data === "graceful-exit") {
app.quit();
}
});
} else {
process.on("SIGTERM", () => {
app.quit();
});
}
}
改造的部分不仅于此,对于窗口配置(上下文隔离打开,不启用Node集成,webSecurity 为 true,引入预加载文件)
这里我的说一下,之前项目情况,本地设备在内网运行,安全问题不在考虑范围内。所以都是安全等级最低的配置。如果改用服务的方式,前端页面要与electron完全隔离,不在可能直接调用node里的方法。
window.ts
const isOpenContextIsolation = true; //是否开启上下文隔离
......
webPreferences: {
contextIsolation: isOpenContextIsolation, //上下文隔离
nodeIntegration: !isOpenContextIsolation, //启用Node集成(是否完整的支持 node)
webSecurity: true, // 确保 webSecurity 为 true
// 引入预加载文件
preload: path.join(__dirname, "preload.js"),
},
preload.ts
const { contextBridge, ipcRenderer } = require("electron");
console.log("============preload......");
// 定义API接口
interface API {
send: (channel: string, data: any) => void;
on: (
channel: string,
func: (...args: any[]) => void
) => (() => void) | undefined;
}
// 白名单机制来验证 channel
const validChannels = [
"readFile",
"writeFile",
"log",
"deleteFile",
"readFileDir",
"window-reset",
];
function removePrefix(str: string) {
const prefix = "reply-";
if (str.startsWith(prefix)) {
return str.replace(prefix, "");
}
return str;
}
// 使用 contextBridge 暴露有限的功能到渲染器进程
contextBridge.exposeInMainWorld("api", {
send: (channel: string, data: any): void => {
if (validChannels.includes(removePrefix(channel))) {
ipcRenderer.send(channel, data);
}
},
on: (
channel: string,
func: (...args: any[]) => void
): (() => void) | undefined => {
if (validChannels.includes(removePrefix(channel))) {
// @ts-ignore
return ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
} as API);
之前都没有使用到preload,对了还有vite配置preload
vite.config.ts
electron({
main: {
entry: "electron-main/index.ts", // 主进程文件
},
preload: {
input: path.join(__dirname, "./electron-preload/index.ts"), // 预加载文件
},
}),
最后npm run build
发现会报错
ReferenceError: require is not defined at index.ts:64:29
全局搜索检查了也没发现那里我使用了 require 导致报错。
报错提示指向的位置是路由的import关键字以及import.meta.glob这两处地方,我不明白,有大佬解惑吗?(我是使用vite自动生成路由的,后面改成导入路由,倒是不报错了,但是页面是白屏。显然也是哪里出问题了)
const comModulesUser = import.meta.glob(`../views/user/**/index.vue`);
......
component: () => import("@/views/user/index.vue")
思路2(loadFile下使用cookie)测试通过
如果是使用loadFile,那么就只能在Electron上动手脚了。
思路是在请求发送后,服务器请求返回cookie时,拦截响应头并处理 Set-Cookie;
后面发送请求时,拦截请求头并添加 Cookie。进行一个欺骗模拟cookie已经设置成功效果。
// 存储 Cookies 的对象
const cookiesStore: Record<string, string> = {};
// 拦截响应头并处理 Set-Cookie
const filter = {
urls: ["http://127.0.0.1:180800/*"],
};
session.defaultSession.webRequest.onHeadersReceived(
filter,
(details, callback) => {
const responseHeaders = details.responseHeaders || {};
for (let header of Object.keys(responseHeaders)) {
if (header.toLowerCase() === "set-cookie") {
responseHeaders[header].forEach((cookie) => {
const cookieParts = cookie.split(";").map((part) => part.trim());
const cookieNameValue = cookieParts[0];
const [name, value] = cookieNameValue.split("=");
cookiesStore[name] = value;
});
responseHeaders[header] = responseHeaders[header].map((cookie) =>
cookie.replace(/Secure/gi, "").replace(/HttpOnly/gi, "")
);
}
}
callback({ responseHeaders });
}
);
// 拦截请求头并添加 Cookie
session.defaultSession.webRequest.onBeforeSendHeaders(
filter,
(details, callback) => {
let cookieString = "";
for (const [name, value] of Object.entries(cookiesStore)) {
cookieString += `${name}=${value}; `;
}
if (cookieString.length > 0) {
details.requestHeaders.Cookie = cookieString.slice(0, -2); // Remove trailing semicolon and space
}
callback({ requestHeaders: details.requestHeaders });
}
);
最终测试思路2成功通过!
更多推荐
所有评论(0)