如何流式加密1GB+大文件并上传,无需全量加载至内存?
流式加密上传大文件解决方案
核心思路
- 借助
File.stream()获取文件可读流,全程避免将1GB+文件加载到内存 - 通过自定义
TransformStream实现AES-CBC流式加密(需处理块对齐与PKCS#7填充) - 根据浏览器兼容性,选择Fetch(现代浏览器)或XHR(兼容旧环境)方案上传加密流
AES-CBC流式加密TransformStream实现
// 生成AES-CBC所需的16字节随机IV function generateIV() { return crypto.getRandomValues(new Uint8Array(16)); } // 创建AES-CBC加密转换流 async function createAesCbcEncryptStream(key, iv) { const cryptoKey = await crypto.subtle.importKey( 'raw', key, { name: 'AES-CBC' }, false, ['encrypt'] ); let leftover = new Uint8Array(0); const blockSize = 16; // AES固定块大小 return new TransformStream({ async transform(chunk, controller) { // 合并上一次剩余数据与当前块 const combined = new Uint8Array(leftover.length + chunk.length); combined.set(leftover); combined.set(new Uint8Array(chunk), leftover.length); // 提取可完整加密的块 const fullBlockCount = Math.floor(combined.length / blockSize); const fullData = combined.slice(0, fullBlockCount * blockSize); leftover = combined.slice(fullBlockCount * blockSize); if (fullData.length > 0) { const encrypted = await crypto.subtle.encrypt( { name: 'AES-CBC', iv }, cryptoKey, fullData ); controller.enqueue(new Uint8Array(encrypted)); } }, async flush(controller) { // 最后一块做PKCS#7填充 const paddingLength = blockSize - leftover.length % blockSize; const paddedData = new Uint8Array(leftover.length + paddingLength); paddedData.set(leftover); paddedData.fill(paddingLength, leftover.length); const encrypted = await crypto.subtle.encrypt( { name: 'AES-CBC', iv }, cryptoKey, paddedData ); controller.enqueue(new Uint8Array(encrypted)); } }); }
方案1:现代浏览器Fetch流式上传
直接将加密后的ReadableStream作为请求体,需将IV传递给服务器(解密必需):
async function uploadWithFetch(file, encryptionKey) { const iv = generateIV(); const encryptStream = await createAesCbcEncryptStream(encryptionKey, iv); const encryptedStream = file.stream().pipeThrough(encryptStream); try { const response = await fetch('/upload', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', // IV转base64放在请求头,服务器解密时需要 'X-Encryption-IV': btoa(String.fromCharCode(...iv)) }, body: encryptedStream }); response.ok ? console.log('上传成功') : console.error('上传失败'); } catch (err) { console.error('请求异常:', err); } }
方案2:兼容旧浏览器的XHR分块上传
XHR不直接支持ReadableStream,可通过ReadableStreamDefaultReader逐块读取并发送:
async function uploadWithXHR(file, encryptionKey) { const iv = generateIV(); const encryptStream = await createAesCbcEncryptStream(encryptionKey, iv); const reader = file.stream().pipeThrough(encryptStream).getReader(); const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload'); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('X-Encryption-IV', btoa(String.fromCharCode(...iv))); xhr.setRequestHeader('Transfer-Encoding', 'chunked'); // 启用分块传输 xhr.onload = () => { (xhr.status >= 200 && xhr.status < 300) ? console.log('上传成功') : console.error('上传失败'); }; xhr.onerror = () => console.error('XHR请求异常'); xhr.send(); // 逐块读取并发送 async function readAndSend() { try { const { done, value } = await reader.read(); if (done) { xhr.send(null); // 发送结束信号 return; } xhr.send(value); await readAndSend(); } catch (err) { console.error('流读取异常:', err); xhr.abort(); } } await readAndSend(); }
兼容性适配建议
- 先检测浏览器支持性:
typeof Request !== 'undefined' && 'body' in Request.prototype && typeof ReadableStream !== 'undefined',满足则用Fetch方案,否则降级到XHR - 服务器需配合处理:接收分块数据并持久化,使用请求头中的IV与密钥解密(注意密钥需通过安全渠道传递,禁止明文传输)
内容的提问来源于stack exchange,提问作者Stef
相关产品推荐
相关产品推荐

