GridFS 本身不加密,必须在写入前或读取后做 AES 加密;流式加密不能直接套用 gridfs.openUploadStream(),得自己接管 chunk 级数据流。

GridFS 本身不加密,必须在写入前或读取后做 AES 加密;流式加密不能直接套用 gridfs.openUploadStream(),得自己接管 chunk 级数据流。
为什么不能直接对 GridFS 流做 AES 加密
GridFS 的 openUploadStream() 返回的是一个可写流(WriteStream),它内部自动分块、写入 fs.chunks,但你无法在 chunk 写入前插入加密逻辑。AES 加密要求输入是完整明文块或可控大小的字节流,而 GridFS 驱动不暴露 chunk 边界控制权。
- 常见错误:把
fs.createReadStream()直接 pipe 给 AES 加密流,再 pipe 给uploadStream—— 这会导致加密后数据长度变化,破坏 GridFS 的 chunk size 对齐,后续read()会解包失败 - 根本限制:GridFS 协议假设写入的是原始文件字节,不校验内容,也不允许中间修改 chunk 结构
- 正确路径只有一条:在写入 GridFS 前完成端到端加密,且保证加密输出仍是可流式消费的字节序列
Node.js 中实现上传时流式 AES 加密
核心是绕过 openUploadStream(),改用 bucket.openUploadStreamWithId() + 手动分块 + 每块 AES 加密后写入,同时维护元数据中记录加密参数。
- 用
crypto.createCipheriv()创建 AES-CTR 或 AES-GCM 模式加密器(推荐 GCM,带认证) - 读取源文件时按固定大小(如 1MB)分块:
readStream.read(1024 * 1024),每块单独加密,避免内存堆积 - 加密块必须保持长度不变(CTR 模式满足)或显式记录 authTag(GCM 模式需把 tag 附在块尾)
- 调用
bucket.openUploadStreamWithId(fileId, { chunkSizeBytes: 1024 * 1024 }),确保 chunk 大小与你分块一致 - 加密后的块写入 uploadStream 之前,先调用
uploadStream.write(encryptedChunk),最后uploadStream.end() - 务必在
metadata中存入:{ encryption: "aes-256-gcm", iv: base64_iv, key_id: "k123" },供后续解密用
Java(Spring Boot + mongo-java-driver)中加密上传要点
Java 客户端没有内置流式加密封装,必须手动组合 GridFSBucket、CipherOutputStream 和分块逻辑。
- 不要用
gridFsTemplate.store():它走的是全量 byte[] 路径,大文件直接 OOM - 用
GridFSBucket.uploadFromStream()时,传入自定义InputStream子类,在read(byte[] b, int off, int len)中完成“读原块 → 加密 → 返回密文”闭环 - AES 密钥不能硬编码,应从 KMS(如 HashiCorp Vault)动态获取,
key_id存进metadata字段 - GCM 模式下,每个块的
authTag必须和密文一起写入,建议格式为:[ciphertext][4-byte-tag-len][tag],解密时按此解析 - 注意
SecureRandom初始化 IV:每次加密新块都应生成新 IV,但要随块一起存,不能复用
读取时如何安全解密并防止中间人篡改
解密不是简单逆向加密,关键在于验证完整性——尤其是 GCM 模式,缺失 authTag 校验等于没加密。
- 先查
fs.files文档,确认metadata.encryption === "aes-256-gcm",提取iv和key_id - 用
bucket.openDownloadStream(fileId)获取GridFSDownloadStream,但它返回的是原始密文流,需包装成CipherInputStream(Java)或crypto.createDecipheriv()(Node.js) - Node.js 示例关键片段:
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(tagBuffer); // 必须在 write 前设置! downloadStream.pipe(decipher).pipe(res); - Java 中若用
doFinal()解密整块,必须捕获AEADBadTagException,这是篡改的明确信号,应立即中断响应 - 别忽略 HTTP 层:即使服务端解密成功,也要用 HTTPS 传输解密后的内容,否则前端拿到的仍是明文裸奔
最容易被忽略的是加密粒度与 GridFS chunk 的对齐问题:AES-GCM 加密一块 256KB 的 chunk,输出长度会增加 16 字节(tag),这会导致整个文件的 chunk 数量错位,fs.files.length 不再准确。解决方案只有两个:要么统一用 CTR 模式(密文长度=明文),要么在 metadata 中额外存一个 encrypted_length 字段替代原始 length 字段用于校验。


















