礼账小程序的数据安全方案:加密与备份

前言

做涉及个人隐私的小程序,数据安全是绕不开的话题。礼金数据涉及"谁给了多少钱""社交关系网",用户对安全很敏感。本文记录礼账小程序的数据安全方案,给做隐私类应用的开发者参考。

安全威胁分析

威胁 风险 应对
服务器故障 数据丢失 多机房备份
传输被窃听 隐私泄露 HTTPS加密
数据库被拖库 隐私泄露 敏感字段加密
手机丢失 他人查看 账号保护+远程擦除
平台跑路 数据丢失 数据导出+本地备份
误删数据 数据丢失 软删除+回收站

1. 多机房备份

云开发数据库默认3副本,自动故障转移。额外加每日全量备份:

javascript 复制代码
// 云函数:每日备份(定时触发)
const cloud = require('wx-server-sdk')
cloud.init()

exports.main = async (event, context) => {
  const db = cloud.database()
  const collections = ['users', 'books', 'contacts', 'gift_records']
  const dateStr = formatDate(new Date())
  
  for (const col of collections) {
    await backupCollection(col, dateStr)
  }
  
  return { success: true, date: dateStr }
}

async function backupCollection(collectionName, dateStr) {
  const db = cloud.database()
  const backupCol = `${collectionName}_backup_${dateStr}`
  
  // 分页导出
  const PAGE_SIZE = 100
  let offset = 0
  let hasMore = true
  
  while (hasMore) {
    const data = await db.collection(collectionName)
      .skip(offset)
      .limit(PAGE_SIZE)
      .get()
    
    if (data.data.length === 0) {
      hasMore = false
      break
    }
    
    // 批量写入备份集合
    for (const record of data.data) {
      await db.collection(backupCol).add({ data: record })
    }
    
    offset += PAGE_SIZE
    
    if (data.data.length < PAGE_SIZE) {
      hasMore = false
    }
  }
  
  console.log(`备份完成: ${collectionName} -> ${backupCol}`)
}

function formatDate(d) {
  return `${d.getFullYear()}${(d.getMonth()+1).toString().padStart(2,'0')}${d.getDate().toString().padStart(2,'0')}`
}

定时触发配置:

json 复制代码
// trigger.json
{
  "triggers": [
    {
      "name": "dailyBackup",
      "type": "timer",
      "config": "0 0 3 * * * *"
    }
  ]
}

2. 敏感字段加密

传输层用HTTPS(云开发默认支持),应用层对敏感字段加密:

javascript 复制代码
// utils/crypto.js
const crypto = require('crypto')

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY  // 32字节密钥

function encrypt(text) {
  const iv = crypto.randomBytes(16)
  const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv)
  let encrypted = cipher.update(text, 'utf8', 'hex')
  encrypted += cipher.final('hex')
  return iv.toString('hex') + ':' + encrypted
}

function decrypt(encryptedText) {
  const parts = encryptedText.split(':')
  const iv = Buffer.from(parts[0], 'hex')
  const encrypted = parts[1]
  const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv)
  let decrypted = decipher.update(encrypted, 'hex', 'utf8')
  decrypted += decipher.final('utf8')
  return decrypted
}

module.exports = { encrypt, decrypt }

使用:

javascript 复制代码
// 插入记录时加密备注(可能含敏感信息)
const { encrypt } = require('./utils/crypto')

await db.collection('gift_records').add({
  data: {
    contactName: '张三',           // 姓名不加密(需查询)
    amount: 80000,                 // 金额不加密(需统计)
    remark: encrypt('父亲的同事'),  // 备注加密
    encryptedFields: ['remark']    // 标记哪些字段加密了
  }
})

3. 账号保护

授权登录

javascript 复制代码
// cloudfunctions/login/index.js
exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  
  return {
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID,
    env: wxContext.ENV
  }
}

远程擦除

手机丢失后,用户可以在其他设备登录后远程擦除:

javascript 复制代码
// cloudfunctions/remoteWipe/index.js
exports.main = async (event, context) => {
  const openid = cloud.getWXContext().OPENID
  const db = cloud.database()
  
  // 删除所有数据
  await db.collection('gift_records').where({ createdBy: openid }).remove()
  await db.collection('contacts').where({ ownerId: openid }).remove()
  await db.collection('books').where({ ownerId: openid }).remove()
  
  // 记录擦除日志
  await db.collection('security_logs').add({
    data: {
      openid,
      action: 'remote_wipe',
      timestamp: new Date(),
      ip: cloud.getWXContext().CLIENTIP
    }
  })
  
  return { success: true }
}

4. 软删除+回收站

javascript 复制代码
// ❌ 硬删除:数据彻底没了
await db.collection('gift_records').doc(id).remove()

// ✅ 软删除:可恢复
await db.collection('gift_records').doc(id).update({
  data: {
    isDeleted: true,
    deletedAt: new Date(),
    deletedBy: openid
  }
})

// 查询时过滤已删除的
const records = await db.collection('gift_records')
  .where({ bookId, isDeleted: false })
  .get()

// 回收站:查看已删除的记录
const deletedRecords = await db.collection('gift_records')
  .where({ bookId, isDeleted: true })
  .get()

// 恢复
await db.collection('gift_records').doc(id).update({
  data: { isDeleted: false, deletedAt: null, restoredAt: new Date() }
})

// 30天后真正删除(定时任务)
async function cleanOldDeletedRecords() {
  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
  
  const oldRecords = await db.collection('gift_records')
    .where({ isDeleted: true, deletedAt: db.command.lt(thirtyDaysAgo) })
    .limit(100)
    .get()
  
  for (const record of oldRecords.data) {
    await db.collection('gift_records').doc(record._id).remove()
  }
}

5. 数据导出

给用户数据控制权,支持随时导出:

javascript 复制代码
// cloudfunctions/exportData/index.js
const XLSX = require('xlsx')

exports.main = async (event, context) => {
  const { bookId } = event
  const openid = cloud.getWXContext().OPENID
  const db = cloud.database()
  
  // 查询数据
  const records = await db.collection('gift_records')
    .where({ bookId, isDeleted: false })
    .orderBy('date', 'asc')
    .get()
  
  // 生成Excel
  const data = records.data.map((r, i) => ({
    '序号': i + 1,
    '姓名': r.contactName,
    '金额(元)': r.amount / 100,
    '类型': r.type === 'receive' ? '收礼' : '送礼',
    '日期': new Date(r.date).toLocaleDateString(),
    '备注': r.remark || ''
  }))
  
  const ws = XLSX.utils.json_to_sheet(data)
  const wb = XLSX.utils.book_new()
  XLSX.utils.book_append_sheet(wb, ws, '礼金记录')
  
  const buffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
  
  // 上传到云存储
  const fileName = `exports/${openid}/${bookId}_${Date.now()}.xlsx`
  const uploadRes = await cloud.uploadFile({
    cloudPath: fileName,
    fileContent: buffer
  })
  
  // 返回临时下载链接(1小时有效)
  const urlRes = await cloud.getTempFileURL({
    fileList: [uploadRes.fileID]
  })
  
  return { url: urlRes.fileList[0].tempFileURL }
}

6. 彻底删除

用户有权利彻底删除所有数据:

javascript 复制代码
// cloudfunctions/deleteAllData/index.js
exports.main = async (event, context) => {
  const openid = cloud.getWXContext().OPENID
  const db = cloud.database()
  
  // 需要二次确认
  if (event.confirm !== 'DELETE_ALL') {
    return { error: '请确认删除操作' }
  }
  
  // 删除所有记录
  await db.collection('gift_records').where({ createdBy: openid }).remove()
  
  // 删除所有联系人
  await db.collection('contacts').where({ ownerId: openid }).remove()
  
  // 删除所有账本(包括协作的)
  const books = await db.collection('books').where({ 'members.openid': openid }).get()
  for (const book of books.data) {
    const newMembers = book.members.filter(m => m.openid !== openid)
    if (newMembers.length === 0) {
      await db.collection('books').doc(book._id).remove()
    } else {
      await db.collection('books').doc(book._id).update({
        data: { members: newMembers }
      })
    }
  }
  
  // 删除用户信息
  await db.collection('users').doc(openid).remove()
  
  // 记录删除日志(只保留日志,不保留数据)
  await db.collection('security_logs').add({
    data: {
      openid,
      action: 'delete_all_data',
      timestamp: new Date()
    }
  })
  
  return { success: true, message: '所有数据已彻底删除' }
}

7. 安全审计日志

javascript 复制代码
// 记录所有敏感操作
async function logSecurityEvent(openid, action, detail) {
  await db.collection('security_logs').add({
    data: {
      openid,
      action,        // login, export, delete, permission_change, remote_wipe
      detail,
      ip: cloud.getWXContext().CLIENTIP,
      timestamp: new Date()
    }
  })
}

// 用户可查看自己的安全日志
exports.getSecurityLogs = async (event, context) => {
  const openid = cloud.getWXContext().OPENID
  
  return await db.collection('security_logs')
    .where({ openid })
    .orderBy('timestamp', 'desc')
    .limit(50)
    .get()
}

踩坑记录

坑1:加密后无法查询

javascript 复制代码
// ❌ 加密后无法用where查询
db.collection('gift_records').where({ remark: encrypt('xxx') })
// 每次加密结果不同(因为iv随机),永远查不到

// ✅ 需要查询的字段不加密,只加密不需要查询的敏感字段
{ contactName: '张三', remark: encrypt('xxx') }

坑2:云函数密钥管理

javascript 复制代码
// ❌ 密钥硬编码在代码里,不安全
const KEY = 'my_secret_key_123'

// ✅ 用环境变量
const KEY = process.env.ENCRYPTION_KEY

坑3:批量删除超时

javascript 复制代码
// ❌ 一次删除太多数据会超时
await db.collection('gift_records').where({ createdBy: openid }).remove()
// 如果有几千条,可能超时

// ✅ 分批删除
async function batchDelete(collection, where, batchSize = 100) {
  while (true) {
    const records = await db.collection(collection).where(where).limit(batchSize).get()
    if (records.data.length === 0) break
    
    for (const record of records.data) {
      await db.collection(collection).doc(record._id).remove()
    }
  }
}

总结

数据安全的核心不是"不丢不泄露"的承诺,而是给用户控制权:

  1. 多机房备份:数据不丢
  2. 敏感字段加密:即使被拖库也看不到明文
  3. 账号保护:授权+远程擦除
  4. 软删除+回收站:误删可恢复
  5. 数据导出:用户可随时备份到本地
  6. 彻底删除:用户有权利删除所有数据
  7. 安全审计:所有敏感操作可追溯

做隐私类应用,安全方案要"防得住意外,给得了控制权"------既要有技术保障,也要让用户觉得"数据是我的,我能掌控"。


作者简介: 本文由"1024House"技术团队撰写。简记往来是一款专注于人情礼金管理的小程序,支持批量录入、双向差额统计、多人协作和云端备份。搜索"简记往来"即可体验。

相关推荐
JNX_SEMI1 小时前
AT2659S L1频段LNA:0.8dB噪声系数与20dB高增益设计解析
数据库
吠品3 小时前
PG_GBK 与 PG_UTF8 编码冲突:一次数据导入踩坑复盘
数据库
lld9510273 小时前
我做了一个 AI Agent Skill 云:用 MCP 在 Claude Code、Codex、OpenCode 之间同步技能
数据库·人工智能
ttwuai4 小时前
GoFrame 连接 MySQL 报错排查:parseTime、时区和 utf8mb4 怎么配
数据库·mysql·adb
辉灰笔记4 小时前
MySQL8 意外关机/误移data目录导致服务启动失败 PID报错/权限报错 修复文档
linux·数据库·mysql·centos
糖果店的幽灵4 小时前
安全测试从入门到精通 - 环境搭建与基础工具选择
安全·web安全
麦聪聊数据4 小时前
电商大促数据库管控(下):数据需求小时级交付与全链路安全管控
数据库
Joker时代5 小时前
重塑Web3安全防线:Vkey验证器正式登陆安卓与iOS平台,开启数字资产防护新纪元
安全·web3
科技发布5 小时前
有没有安全正规的广告交易平台?推荐传播易APP
安全