真正可靠的做法是将敏感字段加密逻辑收束到mongoose的pre('save')钩子中,仅当this.ismodified()为真且非空时执行加密,禁用箭头函数以确保this指向正确,避免重复加密或绕过变更追踪。

直接对 MongoDB 敏感字段做加密存储,不能靠“存之前手动加一层 encrypt()”——那样既难维护又容易漏、错、重复加密。真正可靠的做法,是把加密逻辑收束到 Mongoose 的 pre('save') 钩子中,并配合字段级控制与守卫条件。
敏感字段加密必须放在 pre('save') 钩子里
这是唯一能确保每次写入(新增/更新)都经过统一加密处理的位置。模型外的路由层手动加密极易遗漏(比如后台管理员直接调 User.create())、或在批量操作(insertMany、findOneAndUpdate)中失效。
-
pre('save')是文档级中间件,对单个文档生效,天然适配字段粒度控制 - 必须用普通函数声明:
schema.pre('save', function(next) { ... }),禁用箭头函数(否则this指向丢失) - 不建议在
pre('validate')或pre('init')中加密——前者尚未保证字段存在,后者甚至还没读取原始输入
只对明文敏感字段加密,避免二次哈希/重复加密
和密码哈希一样,加密也怕“套娃”。如果数据库里已存的是密文,而代码又调了一次 encrypt(),后续解密就会失败。关键是要识别“这次是不是真需要加密”。
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
- 用
this.isModified('idCard')或this.isModified('phone')守卫,仅当字段值变更时才触发加密 - 对空值、
null、undefined提前 return,避免加密空字符串或报错 - 不要在钩子里对
this._doc.idCard操作——它绕过 Mongoose 的变更追踪,isModified会失效 - 加密后直接赋值:
this.idCard = encryptedValue,不是this.set('idCard', ...)(后者可能触发额外验证)
选择加密方式:CSFLE 还是应用层手动加密?
两者适用场景完全不同,混用会导致查询失效或安全缺口。
- 要用
Queryable Encryption(即 CSFLE),必须用 MongoDB 7.0+ + 官方共享库 +autoEncryption配置,且加密字段**只能通过特定查询语法(如$eq)匹配,不能$regex或范围查询 - CSFLE 不支持 Mongoose 中间件——它在驱动层拦截,
pre('save')拿到的已经是密文,无法做字段级逻辑 - 若需灵活查询(比如模糊搜手机号前3位)、或兼容旧版 MongoDB,就只能选应用层加密(如
crypto.createCipheriv),并自行管理密钥和 IV 存储 - 注意:CSFLE 加密字段在聚合、索引、备份中都是密文;应用层加密则完全由你控制,但密钥泄露=全盘崩溃
bcrypt.compare 总失败?先查 user.password 字段是不是合法哈希串
这不是加密问题,但常被误归类——很多开发者把密码加密和敏感字段加密混为一谈,结果调试半天发现是 password 字段存了空字符串、JSON 字符串 "\"$2b$12$...\"" 或被截断成 50 字符,导致 bcrypt.compare 报 Error: Invalid salt。
- 用
console.log(typeof user.password, user.password?.length, user.password?.slice(0,10))快速确认字段内容 - MongoDB 字段长度限制默认 16MB,但实际业务中应给
password设maxlength: 100防止注入式膨胀 - 所有加密/哈希字段,建议在 Schema 中显式标注
select: false,防止意外泄露到 API 响应
真正麻烦的从来不是“怎么加”,而是“什么时候不该加”——比如空值、已加密值、被其他服务绕过中间件直写的数据。守卫条件、字段校验、日志埋点,比加密算法本身更决定成败。










