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

如何流式加密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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 07:42:48