人人都会AI编程

24.4 敏感数据安全:本地存储加密、密码保护、日志脱敏

更新时间:2026-07-11

桌面应用相比纯网页有一个显著差异:用户数据往往直接存储在本地磁盘上,一旦应用被反编译或电脑被他人物理接触,这些数据就可能面临泄露风险。因此,必须在设计阶段就规划好敏感数据的保护策略,而不是等问题发生后再补救。

24.4.1 本地存储加密

不要在本地明文保存任何敏感数据,尤其是这些内容:

  • 用户身份凭证(access token、refresh token、session key)
  • 第三方 API 密钥
  • 用户隐私内容(聊天记录、笔记、财务信息)

最常用的本地加密方案是结合系统原生安全机制和成熟加密库:

使用 safeStorage 保护小量数据

Electron 提供了 safeStorage 模块,它会在 macOS 上使用 Keychain,在 Windows 上使用 DPAPI,在 Linux 上使用 libsecret。适合保存 token 类型的短字符串:

const { safeStorage } = require('electron');

// 保存
const plainText = 'user-refresh-token';
const encrypted = safeStorage.encryptString(plainText);
fs.writeFileSync('token.dat', encrypted);

// 读取
const buffer = fs.readFileSync('token.dat');
const decrypted = safeStorage.decryptString(buffer);

优点:加密密钥由操作系统管理,很难被提取。缺点:不适合大文件,且迁移到不同机器后无法解密(这是安全特性,也能通过用户登录恢复)。

对较大数据使用 AES 加密

如果需要加密整个配置文件或数据库,可以使用 Node.js 的 crypto 模块进行 AES-256-GCM 加密。密钥不能写死在代码里,应该通过用户提供的密码派生:

const crypto = require('crypto');
const algorithm = 'aes-256-gcm';

function deriveKey(password, salt) {
  return crypto.scryptSync(password, salt, 32);
}

function encrypt(text, password) {
  const salt = crypto.randomBytes(16);
  const key = deriveKey(password, salt);
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([salt, iv, tag, encrypted]); // 只需安全存储这个 buffer
}

实际项目中,建议直接使用具备安全审计的加密库(如 libsodium-wrappers),它封装好了算法选择、nonce 管理等复杂细节,降低误用风险。

敏感数据库加密

如果应用使用了 SQLite,推荐使用 @journeyapps/sqlcipherbetter-sqlite3 的加密扩展,直接对数据库文件整体加密。这样即便文件被拷贝走,没有密码也无法读取内部数据。

24.4.2 密码保护

当应用需要用户输入密码时(比如解锁应用、加密解密数据),密码本身绝不能以明文方式持久化。正确的处理链路是:

  1. 用户在界面中输入密码
  2. 立即通过 crypto.scryptpbkdf2 派生为密钥(可以同时生成一个校验哈希)
  3. 只存储校验哈希,丢弃原始密码

验证密码时重复派生过程,比对哈希值是否一致。示例:

const crypto = require('crypto');

function hashPassword(password, salt) {
  return new Promise((resolve, reject) => {
    crypto.scrypt(password, salt, 64, (err, derivedKey) => {
      if (err) reject(err);
      resolve(derivedKey.toString('hex'));
    });
  });
}

// 创建用户密码时
const salt = crypto.randomBytes(16).toString('hex');
const hash = await hashPassword(userInput, salt);
// 保存 salt 和 hash 到本地文件或数据库

// 验证时
const newHash = await hashPassword(userInput, savedSalt);
const isValid = newHash === savedHash;

此外还应做到:

  • 限制密码尝试次数(比如连续失败 5 次后锁定 30 秒),防止暴力破解。
  • 不要在日志中输出密码明文或派生的密钥。
  • 当用户切换应用或锁屏时,如果处于敏感页面,应要求重新输入密码或生物识别验证。

24.4.3 日志脱敏

桌面应用通常需要记录日志以便排查问题,但日志中很容易无意间写入敏感信息:API 响应包含 token、请求参数里有身份证号、甚至用户复制的内容都可能被错误记录。因此必须建立日志脱敏机制。

实用脱敏策略

  1. 使用日志级别隔离:生产环境默认只记录 info 及以上,debug 仅在用户手动开启后使用。
  2. 自动过滤敏感字段:在日志输出前,根据字段名(如 passwordtokensecretauthorization)替换值:
const sensitiveKeys = ['password', 'token', 'secret', 'authorization'];

function sanitize(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  const cleaned = {};
  for (const [key, value] of Object.entries(obj)) {
    if (sensitiveKeys.includes(key.toLowerCase())) {
      cleaned[key] = '***';
    } else {
      cleaned[key] = sanitize(value);
    }
  }
  return cleaned;
}
  1. 避免记录完整 HTTP 请求:如果使用 axios 等库,配置拦截器自动脱敏
axios.interceptors.request.use(config => {
  const logData = { method: config.method, url: config.url };
  // 不记录 headers 或只记录白名单字段
  return config;
});
  1. 正则匹配高权限数据:对可能出现的身份证、手机号、邮箱等字符串,用正则替换中间部分为星号。例如手机号保留前 3 后 4 位,其余脱敏。
  1. 远程日志上传同样必须脱敏:很多应用会将错误日志上传到服务器,这属于高风险操作,必须在上传前做一遍完整的脱敏,甚至完全禁止上传含用户内容的日志。

日志文件本身的保护

本地日志文件应放在应用专属目录(app.getPath('logs')app.getPath('userData') 下),并使用操作系统级别的权限隔离。如果日志包含极为敏感的信息,可以考虑用 safeStorage 对日志文件加密,或者在日志轮转时自动删除超过 7 天的旧日志。


小结:敏感数据安全不是一个单一的技术点,而是一套贯穿存储、交互、运维的习惯。原则就是默认不信任任何本地文件,默认不记录任何隐私数据。在实现每一项功能时都多问自己一句:这个数据如果被拷贝走,会造成多大危害?根据答案选择对应的加密、脱敏和访问控制策略。这样搭建出来的桌面应用,才能面对真实的用户环境依然稳固可靠。