前言
做涉及个人隐私的小程序,数据安全是绕不开的话题。礼金数据涉及"谁给了多少钱""社交关系网",用户对安全很敏感。本文记录礼账小程序的数据安全方案,给做隐私类应用的开发者参考。
安全威胁分析
| 威胁 | 风险 | 应对 |
|---|---|---|
| 服务器故障 | 数据丢失 | 多机房备份 |
| 传输被窃听 | 隐私泄露 | 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()
}
}
}
总结
数据安全的核心不是"不丢不泄露"的承诺,而是给用户控制权:
- 多机房备份:数据不丢
- 敏感字段加密:即使被拖库也看不到明文
- 账号保护:授权+远程擦除
- 软删除+回收站:误删可恢复
- 数据导出:用户可随时备份到本地
- 彻底删除:用户有权利删除所有数据
- 安全审计:所有敏感操作可追溯
做隐私类应用,安全方案要"防得住意外,给得了控制权"------既要有技术保障,也要让用户觉得"数据是我的,我能掌控"。
作者简介: 本文由"1024House"技术团队撰写。简记往来是一款专注于人情礼金管理的小程序,支持批量录入、双向差额统计、多人协作和云端备份。搜索"简记往来"即可体验。