尧图精选

Mongoose 客户端字段级加密(CSFLE/Queryable Encryption)集成实战指南

🕒 发布时间:2026/9/11 5:46:45 📁 来源:尧图网络
Mongoose 客户端字段级加密CSFLE/Queryable Encryption集成实战指南【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose字段级加密Client Side Field Level Encryption简称 CSFLE让 MongoDB 以加密缓冲区的形式存储敏感字段而不是明文。本文以 Mongoose 官方文档 docs/field-level-encryption.md 为主体结合仓库源码lib/schema.js、lib/drivers/node-mongodb-native/connection.js、lib/model.js与测试用例test/encryption/encryption.test.js系统讲解如何在 Mongoose 中声明加密 Schema、自动生成加密配置、管理数据密钥以及如何手动搭建 CSFLE 环境帮助你在应用中安全落地敏感数据加密。一、CSFLE 是什么数据在 MongoDB 中如何呈现CSFLE 是一种在 MongoDB 中把数据以加密格式存储的技术。例如不再把name属性存成明文字符串而是让 MongoDB 将文档中的name以加密缓冲区encrypted buffer形式保存。对一个没有解密权限的客户端而言存储后的文档看起来类似如下文档中name字段呈现为BinData(6, ...){ _id : ObjectId(647a3207661e3a3a1bc3e614), name : BinData(6,ASrIv7XfokKwiCUJEjckOdgCGu6IqavcOWX8hINz29MLvcKDZ4nnjCnPFZG0ftVxMdWgzu6Vdh7ys1uIK1WiaPN0SqpmmtL2rPoqT9gfhADpGDmI60vm0bJepXNY1Gv0), __v : 0 }加密发生在客户端Mongoose 及底层 MongoDB Node.js 驱动所在进程侧写入时驱动在把文档发送给服务器之前自动加密读取时驱动在从服务器取回文档之后自动解密。服务器与传输链路上的任何一方拿到的都是密文这保证了数据库管理员、备份数据或磁盘文件泄露时敏感字段依然安全。二、自动 FLE让 Mongoose 自动生成加密配置Mongoose 支持声明加密 Schema——当这样的 Schema 连接到模型时会在底层利用 MongoDB 的客户端字段级加密CSFLE或 Queryable EncryptionQE。Mongoose 在连接建立时根据已注册的模型自动生成encryptedFieldsMap对应 Queryable Encryption或schemaMap对应 CSFLE并在写入时加密字段、读取时解密字段。2.1 两种加密类型Encryption typesMongoDB 提供两种自动加密实现需要根据业务场景取舍加密类型Schema 选项值查询能力生成的加密配置客户端字段级加密CSFLEcsfle支持确定性加密Deterministic实现等值查询schemaMapJSON Schema 语法可查询加密Queryable EncryptionQEqueryableEncryption支持等式查询equality等语法更简洁encryptedFieldsMap两者的核心区别在于查询能力与配置复杂度选择时应参考 MongoDB 官方的选择 in-use 加密方案指导结合是否需要加密字段上的查询来决定。2.2 声明加密 SchemaDeclaring Encrypted Schemas以下 Schema 声明了两个属性name和ssn。其中ssn使用 Queryable Encryption 加密并配置为支持等值查询const encryptedUserSchema new Schema({ name: String, ssn: { type: String, // 1 encrypt: { keyId: uuid string of key id, queries: { queryType: equality } } } // 2 }, { encryptionType: queryableEncryption });要声明一个字段为加密字段必须完成两步在 Schema 定义中为字段标注加密元数据encrypt对象为整个 Schema 选择一个加密类型并按该类型配置 Schema通过 Schema 选项encryptionType。在仓库源码中encryptionType的合法取值被限定为csfle或queryableEncryptionlib/schema.js#L92 的选项注释并且通过Schema.prototype.encryptionType()设置时会做类型校验lib/schema.js#L761-L768Schema.prototype.encryptionType function encryptionType(encryptionType) { if (arguments.length 0) { return this.options.encryptionType; } if (!(typeof encryptionType string || encryptionType null)) { throw new MongooseError(invalid \encryptionType\: ${encryptionType}); } this.options.encryptionType encryptionType; };不是所有 SchemaType 都支持 CSFLE 和 QE。从源码结构看每个 SchemaType 通过实现autoEncryptionType()来声明自身对应的 BSON 类型基类 lib/schemaType.js#L1937-L1939 默认返回null即不支持而 String 返回string其他如Int32、Double、Decimal128、ObjectId、Boolean、Date、Buffer、BigInt、UUID、Map、Array等也有各自的实现见 lib/schema/string.js、lib/schema/int32.js、lib/schema/double.js、lib/schema/decimal128.js、lib/schema/objectId.js、lib/schema/boolean.js、lib/schema/date.js、lib/schema/buffer.js、lib/schema/bigint.js、lib/schema/uuid.js、lib/schema/map.js、lib/schema/array.js。这意味着name: String这类字段会被自动识别为 BSONstring并纳入加密配置而混合类型Mixed等不支持的类型无法用于加密字段。或者你也可以改用 CSFLE非可查询加密来声明同样的字段const encryptedUserSchema new Schema({ name: String, ssn: { type: String, // 1 encrypt: { keyId: [uuid string of key id], // Make sure this is an array algorithm: AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic } } // 2 }, { encryptionType: csfle });两种写法在encrypt配置上有明显差异务必注意Queryable EncryptionkeyId是单个 UUID 字符串通过queries: { queryType: equality }声明支持等值查询CSFLEkeyId必须是数组[uuid string of key id]并通过algorithm指定加密算法如AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic确定性算法允许对加密后的值做等值匹配。MongoDB 的 CSFLE 还支持随机算法AEAD_AES_256_CBC_HMAC_SHA_512-Random随机算法安全性更高但无法直接参与查询匹配可按需选用。从源码看Mongoose 在Schema.prototype.add()中会对encrypt标注做校验与收集lib/schema.js#L922-L942当嵌套值是另一个 Schema 实例时要求子 Schema 的encryptionType必须与父 Schema 一致否则抛出encryptionType of a nested schema must match the encryption type of the parent schema.当值是含encrypt键的对象时要求父 Schema 已设置encryptionType否则抛出encryptionType must be provided随后把该路径与加密配置存入内部的encryptedFields集合。2.3 注册模型Registering Models加密 Schema 可以注册在全局 mongoose 对象上也可以注册在某个具体连接上关键约束是模型必须在连接建立之前注册。原因是 Mongoose 只在调用connect()、createConnection(uri)、openUri()时才能扫描该连接上已注册的模型并自动生成加密配置。// 在 Mongoose 全局对象上注册模型在调用 connect() 之前注册模型 // 这样 Mongoose 才能在连接时生成 schemaMap。 mongoose.model(User, userSchema); await mongoose.connect(mongodbConnectionString, { // 配置自动加密 autoEncryption: { keyVaultNamespace: datakeys.datakeys, kmsProviders } }); // 在新连接上注册模型先无参调用 createConnection() 创建未连接的连接 // 然后注册模型最后调用 openUri()这样 Mongoose 才能在连接时生成 schemaMap。 const conn mongoose.createConnection(); conn.model(User, userSchema); await conn.openUri(mongodbConnectionString, { // 配置自动加密 autoEncryption: { keyVaultNamespace: datakeys.datakeys, kmsProviders } });如果你在连接建立之后才注册模型就必须自己负责把schemaMap和/或encryptedFieldsMap传给驱动——Mongoose 只能在调用createConnection(uri)、connect(uri)或openUri(uri)时为当时已经注册在连接上的模型自动生成这两份配置。底层实现位于 lib/drivers/node-mongodb-native/connection.js#L319-L331连接建立前会调用_buildEncryptionSchemas()汇总所有加密模型只要检测到存在加密 Schema却未提供autoEncryption选项就会直接抛出错误Must provide \autoEncryption when connecting with encrypted schemas.随后自动把生成的配置注入options.autoEncryption.schemaMap与options.autoEncryption.encryptedFieldsMap再交给MongoClient 使用。2.4 连接与加密选项配置Connecting and configuring encryption options字段级加密在 Mongoose 中的工作原理是为连接上每个加密模型生成 MongoDB 驱动所期望的加密 Schema这在该模型的连接建立时自动发生。Queryable Encryption 与 CSFLE 都需要完成 MongoDB 官方in-use 加密文档中列出的全部配置唯一例外是schemaMap与encryptedFieldsMap这两个选项由 Mongoose 自动生成。你只需要提供其余配置例如const keyVaultNamespace client.encryption; const kmsProviders { local: { key } }; await connection.openUri(mongodb://localhost:27017, { // 配置自动加密 autoEncryption: { keyVaultNamespace: datakeys.datakeys, kmsProviders } });配置要点说明keyVaultNamespace数据密钥data key所在的集合命名空间格式为db.collectionkmsProviders密钥管理系统KMS提供方配置本地开发常用{ local: { key } }其中key是一个 96 字节的 Buffer见下文手动 FLE 示例生产环境应使用云 KMS 或密钥管理服务其余可选配置如extraOptions.cryptSharedLibPath、proxyOptions、tlsOptions等与 MongoDB 驱动行为一致。连接建立后Mongoose 的常规操作增删改查照常工作写入由驱动在发往服务器前自动加密读取由驱动在取回文档后自动解密。对应用代码而言读写 API 与普通模型完全一致加密对上层透明。2.5 判别器Discriminators支持加密模型同样支持判别器const connection createConnection(); const schema new Schema({ name: { type: String, encrypt: { keyId } } }, { encryptionType: queryableEncryption }); const Model connection.model(BaseUserModel, schema); const ModelWithAge model.discriminator(ModelWithAge, new Schema({ age: { type: Int32, encrypt: { keyId: keyId2 } } }, { encryptionType: queryableEncryption })); const ModelWithBirthday model.discriminator(ModelWithBirthday, new Schema({ dob: { type: Int32, encrypt: { keyId: keyId3 } } }, { encryptionType: queryableEncryption }));生成加密配置时Mongoose 会把声明在同一命名空间namespace上的所有判别器合并处理。由此带来两条硬性限制同一字段在不同判别器中声明为不同类型是不被支持的——因为合并后的加密配置中同一路径只能有一种 BSON 类型同一命名空间的所有判别器必须使用相同的加密类型——不可能在同一模型上既配置 CSFLE 又配置 Queryable Encryption。这两条限制在源码中有对应实现_buildEncryptionSchemas()会按命名空间合并模型对非根判别器会逐一检查其 Schema 路径若与根 Schema 或已合并的加密字段冲突则抛出Cannot have duplicate keys in discriminators with encryption. keypathlib/drivers/node-mongodb-native/connection.js#L363-L404。2.6 自动加密配置的生成原理源码视角从源码看连接层把加密模型按命名空间归并到两个容器中lib/drivers/node-mongodb-native/connection.js#L364-L391加密类型为csfle的模型归入csfleMappings加密类型为queryableEncryption的模型归入qeMappings。每个命名空间最终生成一份加密配置CSFLE调用Schema.prototype._buildSchemaMap()lib/schema.js#L1020-L1064把扁平的加密字段路径递归构建成 JSON Schema 结构{ bsonType: object, properties: { ... } }每个加密字段展开为{ encrypt: { ...config, bsonType } }其中bsonType由字段对应 SchemaType 的autoEncryptionType()提供QE调用Schema.prototype._buildEncryptedFields()lib/schema.js#L1004-L1013产出{ fields: [{ path, bsonType, keyId, queries? }] }形式的encryptedFieldsMap。这两份配置随后分别注入autoEncryption.schemaMap与autoEncryption.encryptedFieldsMap交给 MongoDB Node.js 驱动完成真正的加密/解密。三、管理数据密钥Model.clientEncryption()Mongoose 提供便捷 API 来获取一个配置好的ClientEncryption对象用于管理 key vault密钥库中的数据密钥data key。通过Model.clientEncryption()帮助方法即可获得const connection createConnection(); const schema new Schema({ name: { type: String, encrypt: { keyId } } }, { encryptionType: queryableEncryption }); const Model connection.model(BaseUserModel, schema); await connection.openUri(mongodb://localhost:27017, { autoEncryption: { keyVaultNamespace: datakeys.datakeys, kmsProviders: { local: .... } } }); const clientEncryption Model.clientEncryption();拿到ClientEncryption后你可以用它执行createDataKey()创建数据密钥、查询密钥、为密钥关联备用名称keyAltNames等运维操作。从源码看Model.clientEncryption()lib/model.js#L5094-L5125的实现要点从当前驱动的ClientEncryption构造函数创建实例若驱动不支持抛出The mongodb driver must be used to obtain a ClientEncryption object.若底层 client 尚未配置未连接或未配置autoEncryption返回null创建的ClientEncryption复用连接上autoEncryption配置中的keyVaultNamespace、keyVaultClient、kmsProviders、credentialProviders、proxyOptions、tlsOptions——也就是说它和 Mongoose 底层MongoClient使用同一套加密设置无需重复配置。在测试 test/encryption/encryption.test.js#L1215-L1276 中覆盖了未配置autoEncryption时返回null、配置后返回mdb.ClientEncryption实例、可执行createDataKey()与getKeys()以及keyVaultNamespace、kmsProviders、proxyOptions、tlsOptions、credentialProviders、keyVaultClient均正确透传等行为。四、手动 FLE自己搭建完整的加密环境自动 FLE 依赖连接时自动生成配置手动 FLE 则适合你想完全掌控加密配置、或需要先创建密钥再连接的场景。4.1 安装依赖首先安装 MongoDB 官方的加密密钥管理包npm install mongodb-client-encryption同时确保已安装以下之一mongocryptd独立于 MongoDB 服务器的辅助进程用于处理字段级加密。你可以自行启动它也可以确保它位于系统 PATH 中让 MongoDB Node.js 驱动自动拉起crypt_shared 共享库一个动态库可从 MongoDB 企业版下载中心获取用extraOptions.cryptSharedLibPath指定路径。4.2 创建数据加密密钥搭建好 CSFLE 环境后首先需要创建一个新的加密密钥。注意以下示例只是为了帮助你快速上手示例中的本地密钥是不安全的——MongoDB 官方建议使用 KMS密钥管理系统来管理主密钥。const { ClientEncryption } require(mongodb); const mongoose require(mongoose); run().catch(err console.log(err)); async function run() { /* 步骤 1连接 MongoDB 并插入一个密钥 */ // 创建一个非常基础的密钥。你负责保证密钥安全生产环境千万别这么用 :) const arr []; for (let i 0; i 96; i) { arr.push(i); } const key Buffer.from(arr); const keyVaultNamespace client.encryption; const kmsProviders { local: { key } }; const uri mongodb://127.0.0.1:27017/mongoose_test; const conn await mongoose.createConnection(uri, { autoEncryption: { keyVaultNamespace, kmsProviders, // 如果使用 crypt_shared可以通过取消注释下面的代码 // 指定 crypt_shared 的路径。 // SHARED_LIB_PATH 应该是共享库文件的路径而不是 // 共享库文件所在目录的路径。 // extraOptions: { // cryptSharedLibPath: process.env.SHARED_LIB_PATH // } } }).asPromise(); const encryption new ClientEncryption(conn.getClient(), { keyVaultNamespace, kmsProviders, }); const _key await encryption.createDataKey(local, { keyAltNames: [exampleKeyName], }); }要点本地 KMS 的key是一个96 字节的 Buffer示例中循环生成了 0~95 共 96 个字节keyVaultNamespace指向存放数据密钥的集合例如client.encryptioncreateDataKey(local, { keyAltNames: [exampleKeyName] })在 key vault 中创建一个数据密钥keyAltNames是便于后续引用的备用名称返回的_key是对应密钥的 UUIDBSON Binary会用于接下来的schemaMap配置。4.3 使用 schemaMap 连接并加密字段拿到加密密钥后就可以创建另一个 Mongoose 连接用schemaMap以JSON Schema 语法声明哪些字段被加密/* 步骤 2使用 schema map 和新密钥连接 */ await mongoose.connect(mongodb://127.0.0.1:27017/mongoose_test, { // 配置自动加密 autoEncryption: { keyVaultNamespace, kmsProviders, schemaMap: { mongoose_test.tests: { bsonType: object, encryptMetadata: { keyId: [_key] }, properties: { name: { encrypt: { bsonType: string, algorithm: AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic } } } } }, // 如果使用 crypt_shared可以通过取消注释下面的代码 // 指定 crypt_shared 的路径 // extraOptions: { // cryptSharedLibPath: process.env.SHARED_LIB_PATH // } } });schemaMap按数据库名.集合名作为键这里对应数据库mongoose_test中的tests集合值是该命名空间的 JSON SchemaencryptMetadata.keyId声明默认使用哪个数据密钥properties.name.encrypt则具体声明name字段为字符串类型并使用确定性加密算法。有了上述连接如果你创建一个名为Test、使用tests集合的模型任何文档的name属性都会被加密// super secret 将以 BinData 形式存储在数据库中 // 如果用 mongo shell 查询看到的就是密文。 const Model mongoose.model(Test, mongoose.Schema({ name: String })); await Model.create({ name: super secret });这里手动提供的schemaMap与自动 FLE 中由 lib/schema.js 的_buildSchemaMap()生成的配置在结构上完全一致——区别仅在于自动模式下你只需声明encrypt元数据和encryptionType由 Mongoose 完成 JSON Schema 的组装手动模式下 JSON Schema 完全由你书写。五、加密模型的关键注意事项综合官方文档与仓库源码使用字段级加密时有以下几点需要特别注意注册时机加密模型必须在connect()/createConnection(uri)/openUri()之前注册否则必须手工提供schemaMap/encryptedFieldsMap必须提供autoEncryption连接包含加密模型却不传autoEncryption选项时Mongoose 会直接抛错lib/drivers/node-mongodb-native/connection.js#L321-L323加密类型一致性嵌套 Schema 的encryptionType必须与父 Schema 一致同一命名空间的判别器必须使用同一种加密类型判别器键唯一同一命名空间内判别器不能对同一路径声明不同类型会抛Cannot have duplicate keys in discriminators with encryptionSchemaType 支持面只有实现了autoEncryptionType()的 SchemaType 才能用于加密字段基类默认返回null表示不支持CSFLE 的keyId是数组QE 的keyId是单个 UUID 字符串两者不能混用主密钥安全本地 KMS 密钥示例仅用于开发调试生产环境务必接入 KMS 服务。六、可深入阅读的仓库资源官方集成文档docs/field-level-encryption.md加密配置校验与生成Schema.prototype.encryptionType()、_buildEncryptedFields()、_buildSchemaMap()lib/schema.js#L756-L768、lib/schema.js#L1004-L1064连接层自动注入schemaMap/encryptedFieldsMaplib/drivers/node-mongodb-native/connection.js#L319-L404Model.clientEncryption()实现lib/model.js#L5094-L5125各 SchemaType 的 BSON 类型映射lib/schema/string.js、lib/schema/int32.js、lib/schema/double.js 等完整的端到端测试含判别器、数据密钥管理、加密配置透传等场景test/encryption/encryption.test.js【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →