当前位置: 首页
数据库
Electron使用electron-store实现加密存储与自定义格式文件

Electron使用electron-store实现加密存储与自定义格式文件

热心网友 时间:2026-07-19
转载

在Electron应用中,利用electron-store的序列化 反序列化钩子,结合crypto-js进行AES加密,将数据以Hex文本格式存储为自定义扩展名文件,从而避免JSON明文暴露。数据读写仅限主进程,渲染进程通过IPC通信访问,实现加密存储与文件格式自定义。

背景与目标

桌面端的验配或业务软件,经常需要在本地保存结构化数据,比如客户信息、业务记录之类的。与此同时,有几个诉求几乎是刚需:

  1. 文件打开后不能直接阅读——至少得挡住随手改 JSON 的那种操作。
  2. 扩展名别用默认的 .json,降低用户误开、误传、误改的概率。
  3. 读写只发生在主进程,渲染进程通过 IPC 访问,不暴露文件系统能力。
  4. API 用起来像操作内存对象get / set 就行,不用手写一堆 fs.readFileJSON.parse

默认情况下的 electron-store,说白了就是把明文 JSON 往 userData 里一扔,扩展名还是 .json,这显然不符合前面说的 1 和 2 两条要求。不过好在它提供了一些关键的配置项:

  • fileExtension:自定义后缀
  • serialize / deserialize:自定义序列化管线
  • cwd:自定义落盘目录

这两条钩子,就是整套方案真正的核心所在。

最终效果示意

 复制代码安装目录/
├── YourApp.exe
└── data/
    ├── app-records.dat      ← 加密后的 hex 文本,扩展名自定(这里假设为dat)
    └── app-entities.dat

用记事本打开 .dat 文件,你能看到的只是一长串十六进制字符,绝不是明文 JSON。


总体架构

 复制代码┌─────────────────┐     IPC      ┌──────────────────┐
│  Renderer (Vue) │ ──────────►  │  Main Process    │
│  只调业务 API    │             │  entity-store.ts │
└─────────────────┘             │         │         │
                                │         ▼         │
                                │ createEncryptedStore()
                                │  serialize → AES → hex
                                │  deserialize ← 解密
                                │         │         │
                                │         ▼         │
                                │  data/*.dat 文件   │
                                └──────────────────┘

再来看看分层建议:

职责
encrypted-store 工具加解密 + 创建加密 Store
xxx-store 业务模块Schema、CRUD、软删除等
xxx-ipc暴露给渲染进程的通道
(可选)backup / import复用同一套加解密做备份包

第一步:安装依赖

记住,这一步是在 Electron 主进程侧进行的,别把它塞进渲染进程的打包依赖里。

 复制代码npm install electron-store crypto-js
npm install -D @types/crypto-js   # 若使用 TypeScript

简单说明一下:

  • electron-store:负责键值存取、原子写入和默认值。
  • crypto-js:在主进程里做 AES 加密解密。当然,你也可以换成 Node 自带的 crypto 模块,思路是一样的。

第二步:确定数据目录

业务数据如果希望“随安装目录走,方便拷贝或备份”,那就别用默认的 app.getPath('userData')。可以根据打包状态做个分支:

 复制代码// electron/utils/app-paths.ts(示意)
import { mkdirSync } from 'node:fs'
import path from 'node:path'
import { app } from 'electron'/** 打包后:安装目录;开发时:项目根 */
export function resolveAppInstallDir(): string {
  return app.isPackaged
    ? path.dirname(app.getPath('exe'))
    : path.join(process.env.APP_ROOT ?? process.cwd())
}/** 确保 /data 存在并返回 */
export function resolveAppDataDir(): string {
  const dir = path.join(resolveAppInstallDir(), 'data')
  mkdirSync(dir, { recursive: true })
  return dir
}

这样一来,开发环境的数据就在仓库旁的 data/ 目录里,发布后则在 .exe 同级的 data/ 目录下,运维人员定位文件会直观很多。


第三步:实现加解密管线

设计的核心目标很明确:

  1. 在内存中,数据始终是普通的 JS 对象。
  2. 落盘时,流程是 JSON.stringify → AES 加密 → Hex 文本 写入文件。
  3. 读取时,顺序反过来:Hex → AES 解密 → JSON.parse

为什么要用 Hex?因为文件全程都是可打印的 ASCII 字符,复制、日志截断、跨平台换行都更稳定。代价是文件体积会翻倍,但对于本机业务库来说,通常可以接受。

 复制代码// electron/utils/encrypted-store.ts(示意,密钥请勿写死在仓库)
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
import path from 'node:path'import CryptoJS from 'crypto-js'
import Store, { type Options as StoreOptions } from 'electron-store'import { resolveAppDataDir } from './app-paths'/**
 * 密钥读取示意:生产环境应来自安全配置 / 构建注入 / 系统凭据,
 * 切勿把真实密钥提交到 Git。
 */
function getStoreSecret(): string {
  const secret = process.env.APP_STORE_SECRET
  if (!secret) {
    throw new Error('缺少 APP_STORE_SECRET')
  }
  return secret
}/** 明文 → AES → Base64 密文 → 再编码为 Hex 字符串落盘 */
function encryptToHex(plainText: string): string {
  const encrypted = CryptoJS.AES.encrypt(plainText, getStoreSecret()).toString()
  return CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(encrypted))
}/** Hex 文件内容 → 解密得到明文 JSON 字符串 */
function decryptFromHex(hex: string): string {
  const encrypted = CryptoJS.enc.Hex.parse(hex).toString(CryptoJS.enc.Utf8)
  const decrypted = CryptoJS.AES.decrypt(encrypted, getStoreSecret())
  const result = decrypted.toString(CryptoJS.enc.Utf8)
  if (!result) {
    throw new Error('解密 store 数据失败(密钥错误或文件损坏)')
  }
  return result
}/** 供备份导出等「不经过 Store」场景复用同一算法 */
export function encryptStoreContent(plainText: string): string {
  return encryptToHex(plainText)
}export function decryptStoreContent(hex: string): string {
  return decryptFromHex(hex)
}

第四步:封装 createEncryptedStore

把自定义扩展名、目录、序列化这些配置一次性绑死,业务侧只需要传 namedefaults 就行了:

 复制代码const CUSTOM_EXT = 'dat' // 示例后缀;本项目可使用自有扩展名/** 旧版明文 .json → 新版加密文件(一次性迁移) */
function migrateLegacyJsonStore(dataDir: string, name: string): void {
  const encryptedPath = path.join(dataDir, `${name}.${CUSTOM_EXT}`)
  const jsonPath = path.join(dataDir, `${name}.json`)
  if (existsSync(encryptedPath) || !existsSync(jsonPath)) return  const raw = readFileSync(jsonPath, 'utf8')
  writeFileSync(encryptedPath, encryptToHex(raw), 'utf8')
  unlinkSync(jsonPath)
}/** 创建主进程专用加密 Store */
export function createEncryptedStoreextends Record<string, unknown>>(
  options: Omit<StoreOptions, 'serialize' | 'deserialize' | 'fileExtension' | 'cwd'>
): Store {
  const dataDir = resolveAppDataDir()
  const name = options.name ?? 'config'
  migrateLegacyJsonStore(dataDir, name)  return new Store({
    ...options,
    cwd: dataDir,
    fileExtension: CUSTOM_EXT,
    serialize: value => encryptToHex(JSON.stringify(value)),
    deserialize: text => JSON.parse(decryptFromHex(text)) as T,
  })
}

关键配置项说明:

选项作用
cwd落到 data/,而不是默认的 userData
fileExtension例如 dat 或项目自定义后缀
serialize写盘前加密
deserialize读盘后解密
迁移函数老用户仍有明文 .json 时,能平滑升级

需要特别注意的是,业务代码从此禁止绕过这层直接读文件,否则加解密格式很容易分裂。


第五步:业务 Store 怎么写

以“实体列表”为例,字段已经抽象过:

 复制代码// electron/hooks/entity-store.ts(示意)
import { randomUUID } from 'node:crypto'
import { createEncryptedStore } from '../utils/encrypted-store'interface EntityRecord {
  id: string
  title: string
  updatedAt: string
  deletedAt?: string | null
}interface EntityStoreSchema {
  entities: EntityRecord[]
}const store = createEncryptedStore<EntityStoreSchema>({
  name: 'app-entities',
  defaults: {
    entities: [],
  },
})export function listEntities(): EntityRecord[] {
  return store
    .get('entities')
    .filter(item => !item.deletedAt)
    .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
}export function addEntity(title: string): EntityRecord {
  const now = new Date().toISOString()
  const record: EntityRecord = {
    id: randomUUID(),
    title,
    updatedAt: now,
  }
  const entities = store.get('entities')
  store.set('entities', [record, ...entities])
  return record
}/** 软删除:保留痕迹便于审计 / 导入合并 */
export function softDeleteEntity(id: string): void {
  const entities = store.get('entities')
  const index = entities.findIndex(item => item.id === id)
  if (index < 0) throw new Error('记录不存在')  const next = [...entities]
  next[index] = {
    ...entities[index],
    deletedAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  }
  store.set('entities', next)
}

这里有几个实践中的建议:

  • Schema 显式声明:用 Store,避免 any 到处蔓延。
  • 列表整体读写:对于小规模的本机库,这种做法够用了。更新字段时,拷贝一份再 set,不要原地 mutate 后指望它自动落盘。
  • 软删除:在导入合并、审计等场景下,软删除往往比物理删除更合适。

最终的落盘结果类似这样:

 复制代码data/app-entities.dat   # 一整文件都是 hex

第六步:只通过 IPC 暴露给渲染进程

加密文件只应该由主进程接触。渲染层只需要拿到“业务结果”:

 复制代码// electron/ipc/entity-ipc.ts(示意)
import { ipcMain } from 'electron'
import { addEntity, listEntities, softDeleteEntity } from '../hooks/entity-store'export function registerEntityIpc() {
  ipcMain.handle('entity:list', () => listEntities())
  ipcMain.handle('entity:add', (_e, title: string) => addEntity(title))
  ipcMain.handle('entity:delete', (_e, id: string) => softDeleteEntity(id))
}

渲染进程的调用方式:

 复制代码// 渲染侧示意
const list = await window.api.invoke('entity:list')

配合 contextIsolation 和 preload 白名单通道,就能有效避免渲染进程直接 require('fs') 或者拿到 Store 实例。


第七步:备份 / 导入复用同一套算法

既然有了统一的 encryptStoreContentdecryptStoreContent,备份包就可以做成“外层再包一层加密”的结构:

 复制代码// 备份包结构示意(明文逻辑;落盘前整体再加密)
interface DataBackupBundle {
  version: 1
  exportedAt: string
  files: Record<string, string> // key: 文件名,value: 单库已加密的 hex
}function writeBackup(targetDir: string, bundle: DataBackupBundle): string {
  const fileName = `backup_${Date.now()}_all.dat`
  const outputPath = path.join(targetDir, fileName)
  // 注意:bundle.files 里已经是各 store 的密文;整包 JSON 再加密一层
  writeFileSync(outputPath, encryptStoreContent(JSON.stringify(bundle)), 'utf8')
  return outputPath
}function readBackup(filePath: string): DataBackupBundle {
  const raw = readFileSync(filePath, 'utf8')
  const bundle = JSON.parse(decryptStoreContent(raw)) as DataBackupBundle
  if (!bundle?.files || typeof bundle.files !== 'object') {
    throw new Error('备份文件格式无效')
  }
  return bundle
}

导入时的流程:

  1. 先解密外层备份包。
  2. 再解密各子 store 的 hex,得到 JSON。
  3. 按业务规则进行合并(处理冲突决议、id 映射等)。
  4. 写回本地 Store(仍然走 createEncryptedStore 的序列化)。

这样一来,运行时库、导出包、导入包三套文件的格式完全一致,维护成本降到了最低。


从 0 到 1 的落地清单

可以按下面的顺序,一次性做完:

  1. 安装 electron-storecrypto-js
  2. 实现 resolveAppDataDir(),确认打包后目录正确
  3. 实现 encryptToHex / decryptFromHex,密钥走环境变量或安全配置
  4. 封装 createEncryptedStorecwd + 自定义扩展名 + serialize/deserialize)
  5. 迁移逻辑:旧 .json → 加密文件,只跑一次
  6. 业务 xxx-storedefaults + CRUD + 类型 Schema
  7. IPC 注册;preload 白名单;渲染层只调通道
  8. (可选)备份/导入复用 encryptStoreContent
  9. 手工验证:改几个字节文件应解密失败;get/set 往返数据一致

一个验证片段:

 复制代码const store = createEncryptedStore<{ count: number }>({
  name: 'smoke-test',
  defaults: { count: 0 },
})
store.set('count', 42)
console.log(store.get('count')) // 42
// 打开 data/smoke-test.dat,应看不到明文 42

常见坑

  1. 在渲染进程使用 electron-store
    安全隔离和打包路径都会变得很乱。老老实实放主进程。

  2. 忘记 JSON.stringify 包在加密外层
    serialize 收到的是对象,必须先序列化再加密。

  3. Hex / Base64 搞混
    加密后 CryptoJS 默认输出的是 Base64 字符串,我们这个方案又转了一次 Hex 再落盘。解密时必须严格逆序操作。

  4. 密钥更换
    一旦更换密钥,旧文件就全部不可读了。需要提前规划版本字段或迁移工具。

  5. 大对象频繁整文件 set
    electron-store 适合中小规模的本机库。如果数据量很大,还是考虑 SQLite 之类的方案。

  6. 把真实密钥写进示例博客或仓库
    生产环境的密钥必须独立管理。文档和示例里一律用占位符。


小结

electron-store 本身并不是一个加密方案,但它把“改序列化管线”这个接口开放了出来,这才是关键所在。通过:

  • 自定义 fileExtension → 变成业务自有格式文件
  • serialize / deserialize + AES → 磁盘上不再是明文 JSON
  • cwd 指向安装目录 data/ → 文件位置可控
  • 主进程 Store + IPC → 最小暴露面
  • 备份导入共用算法 → 端到端格式统一

这样就能在不明显牺牲开发体验的前提下,搭建出一套适合桌面业务软件的本地加密存储方案。

来源:https://juejin.cn/post/7662382891234672649

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
利用AWR报告诊断表空间碎片对扫描性能的影响

利用AWR报告诊断表空间碎片对扫描性能的影响

通过AWR报告中dbfilesequentialread等待异常、物理读请求次数增幅远超读块数、以及SQL执行计划从索引扫描退化为全表扫描这三类信号交叉验证,可判断表空间碎片是否拖慢扫描性能,避免误判。

时间:2026-07-19 10:46
MySQL第三方审计系统只读系统视图权限配置方法

MySQL第三方审计系统只读系统视图权限配置方法

为审计账号配置MySQL只读权限时,performance_schema必须逐表显式授权,不可使用* *或库级批量授权;认证插件必须指定mysql_native_password;无法通过视图封装,必须直接授权原始表。这是审计账号配置的关键注意事项,必须严格遵守,并遵循最小权限原则。

时间:2026-07-19 10:45
Navicat团队项目自定义图标背景色设置方法

Navicat团队项目自定义图标背景色设置方法

Navicat中设置团队项目图标背景色实为两个独立配置:模型图节点颜色需手动修改navicat ini文件并完全重启;SQL编辑器及主窗口背景色通过主题设置。版本一致、配置路径准确、激活ERD模式是效果一致的关键。修改后必须彻底退出程序。

时间:2026-07-19 10:14
SQL嵌套查询中如何有效利用索引覆盖提升性能

SQL嵌套查询中如何有效利用索引覆盖提升性能

SQL嵌套查询中,子查询字段未被索引覆盖会导致全表扫描,而外层EXPLAIN的Usingindex可能误导优化。需为子查询过滤字段建索引,联合索引将SELECT字段包含在内且顺序正确。PostgreSQL可用INCLUDE或组合索引,MySQL8 0+支持函数索引,物化视图需手动建索引并刷新统计信息。

时间:2026-07-19 10:14
SQL窗口函数快速查找用户多设备登录顺序

SQL窗口函数快速查找用户多设备登录顺序

使用ROW_NUMBER()配合PARTITIONBYuser_id和ORDERBYlogin_time,可快速按用户分组并排序登录顺序。漏掉PARTITIONBY会导致全局编号,且必须用ROW_NUMBER()保证编号连续,避免RANK()或DENSE_RANK()的跳号问题。区分首次登录可嵌套MIN()窗口函数。老版本MySQL用变量模拟易出错,建议升级

时间:2026-07-19 10:14
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜