You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现MongoDB加密字段的直接查询搜索功能

mongoose-field-encryption 加密字段搜索实现方案

mongoose-field-encryption 默认采用AES-256-CBC概率加密逻辑,同一段明文每次生成的密文都不相同,所以直接写查询条件匹配密文必然搜不到结果。针对邮箱、姓名、手机号码、地址这类需要搜索的加密字段,可以根据自身业务场景从下面三个成熟方案里选,都是生产环境验证过的实现。


方案1:开启确定性加密(适合精确匹配、前缀匹配场景)

这个插件原生支持确定性加密模式,开启后相同明文生成的密文固定,插件会自动把查询条件里传入的明文加密后再匹配库中密文,不需要写额外转换逻辑,最适合邮箱、手机号、姓名这类需要精确查找、前缀匹配的字段。
直接修改Schema配置即可:

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    // 给需要精确搜索的字段开启确定性加密
    encrypt: { deterministic: true }
  },
  phone: {
    type: String,
    encrypt: { deterministic: true }
  },
  name: {
    type: String,
    encrypt: { deterministic: true }
  },
  // 地址如果只需要精确匹配也可以开,要做模糊搜索就走后面的方案
  address: { type: String, encrypt: true }
});

// 插件初始化注意要固定salt生成规则,不能用默认的随机salt
userSchema.plugin(mongooseFieldEncryption, {
  secret: process.env.MONGO_ENC_SECRET,
  saltGenerator: (secret) => secret.slice(0, 16), // 确定性加密必须返回固定salt值
  encryptedFieldType: String
});

查询的时候正常写条件就行,插件会自动处理加密转换逻辑:

// 精确查询对应邮箱的用户
const targetUser = await User.findOne({ email: 'user@example.com' });
// 前缀匹配手机号,比如查询138开头的所有用户
const prefixMatchUsers = await User.find({ phone: /^138/ });

踩坑提醒:确定性加密的安全性比默认的概率加密低,相同明文对应密文一致,拖库后存在被频率分析反推明文的风险,只给有强搜索需求的字段开启,身份证、银行卡这类极高敏感字段不要用这个模式。


方案2:分词哈希索引(适合地址、姓名这类任意位置模糊搜索场景)

地址这类字段经常需要做包含匹配,比如搜“朝阳”要命中“北京市朝阳区”的记录,确定性加密做不到中间/后缀模糊匹配,这种场景可以额外存一份不可逆的分词哈希索引,查询时先把搜索词转成哈希再匹配索引字段,不会泄露明文信息。
实现步骤:

  1. 给每个需要模糊搜索的加密字段加一个数组类型的索引字段,用来存分词后的哈希值
  2. 写pre-save钩子,存数据时把明文按n-gram规则分词,每个分词用HMAC生成哈希存到索引数组
  3. 查询时把用户输入的关键词按同样规则生成哈希,匹配索引数组即可返回结果
    参考实现代码:
const crypto = require('crypto');
const SEARCH_HMAC_KEY = process.env.SEARCH_INDEX_KEY; // 注意和字段加密密钥分开存储

// 生成分词,默认按2-3字切分,粒度可以根据自己的搜索精度调整
function genNGrams(text, gramLen = 2) {
  const gramSet = new Set();
  const cleanText = text.trim().toLowerCase();
  for (let i = 0; i <= cleanText.length - gramLen; i++) {
    gramSet.add(cleanText.slice(i, i + gramLen));
  }
  return Array.from(gramSet);
}

// 分词转HMAC哈希,避免被彩虹表反推明文
function hashGram(gram) {
  return crypto.createHmac('sha256', SEARCH_HMAC_KEY).update(gram).digest('hex');
}

const userSchema = new mongoose.Schema({
  name: { type: String, encrypt: true },
  address: { type: String, encrypt: true },
  // 搜索索引字段不需要加密,存的是不可逆哈希值
  name_search_idx: [{ type: String, index: true }],
  address_search_idx: [{ type: String, index: true }]
});

// 数据保存时自动生成对应的搜索索引
userSchema.pre('save', function(next) {
  if (this.isModified('name')) {
    this.name_search_idx = genNGrams(this.name).map(hashGram);
  }
  if (this.isModified('address')) {
    this.address_search_idx = genNGrams(this.address, 3).map(hashGram);
  }
  next();
});

// 封装搜索静态方法,业务调用更方便
userSchema.statics.fuzzySearchAddress = function(keyword) {
  const targetHash = hashGram(keyword.trim().toLowerCase());
  return this.find({ address_search_idx: targetHash });
};

踩坑提醒:分词粒度越小搜索召回率越高,但对应的存储成本和误匹配概率也会上升,根据业务实际效果调整即可;不要用普通sha256做哈希,一定要用带密钥的HMAC,防止被彩虹表撞出明文。


方案3:内存过滤(适合小数据量内部系统)

如果你的表总数据量在10万条以内,又是内部使用的管理系统,没必要搞上面的复杂逻辑,直接查全量数据(插件会自动解密所有加密字段),在内存里做过滤就行:

// 查询地址里带"朝阳"的所有用户
const allUsers = await User.find({});
const matchedUsers = allUsers.filter(user => user.address.includes('朝阳'));

踩坑提醒:数据量超过10万别用这个方案,全表扫描+全量数据拉到内存很容易把服务打挂,只适合小体量内部工具使用。


通用注意事项

  • 别为了搜索直接关闭字段加密,等于白引入这个加密插件
  • 确定性加密的salt规则一旦确定不要随意修改,不然老数据的密文匹配不上,会直接搜不到
  • 搜索索引用的密钥和字段加密的密钥要分开存储,降低密钥泄露后的风险

内容的提问来源于stack exchange,提问作者Harshal Faldu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 19:12:30