⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

12.5.3 传的数据对不对——完整性校验:MD5/SHA256 哈希验证

一句话破题

完整性校验确保上传的文件没有在传输过程中损坏——客户端计算 hash,服务端验证 hash,不一致就重传。

校验层级

层级校验对象目的
分片级单个分片快速定位损坏分片
文件级整个文件确保最终文件完整

分片级校验

typescript
async function calculateChunkHash(chunk: Blob): Promise<string> {
  const buffer = await chunk.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}

async function uploadChunkWithVerification(chunk: Chunk, fileId: string) {
  const hash = await calculateChunkHash(chunk.blob);

  const formData = new FormData();
  formData.append('chunk', chunk.blob);
  formData.append('index', chunk.index.toString());
  formData.append('fileId', fileId);
  formData.append('hash', hash);

  const response = await fetch('/api/upload/chunk', {
    method: 'POST',
    body: formData,
  });

  const result = await response.json();
  if (!result.verified) {
    throw new Error(`分片 ${chunk.index} 校验失败`);
  }
}

服务端验证

typescript
// app/api/upload/chunk/route.ts
import { createHash } from 'crypto';

export async function POST(req: Request) {
  const formData = await req.formData();
  const chunk = formData.get('chunk') as File;
  const clientHash = formData.get('hash') as string;
  const index = formData.get('index') as string;
  const fileId = formData.get('fileId') as string;

  const buffer = Buffer.from(await chunk.arrayBuffer());

  // 计算服务端 hash
  const serverHash = createHash('sha256').update(buffer).digest('hex');

  if (serverHash !== clientHash) {
    return Response.json({ verified: false, error: 'Hash mismatch' }, { status: 400 });
  }

  // 保存分片
  const chunkDir = join(process.cwd(), 'uploads', fileId);
  await mkdir(chunkDir, { recursive: true });
  await writeFile(join(chunkDir, index), buffer);

  return Response.json({ verified: true });
}

文件级校验

合并后验证整个文件:

typescript
// 客户端:计算整个文件的 hash
async function calculateFileHash(file: File): Promise<string> {
  const chunkSize = 10 * 1024 * 1024; // 10MB 分块处理避免内存问题
  let offset = 0;
  const hashParts: ArrayBuffer[] = [];

  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    hashParts.push(await chunk.arrayBuffer());
    offset += chunkSize;
  }

  // 使用 Web Crypto API
  const combined = await new Blob(hashParts.map((p) => new Uint8Array(p))).arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', combined);
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

// 上传完成后验证
async function verifyUpload(fileId: string, expectedHash: string) {
  const response = await fetch(`/api/upload/verify?fileId=${fileId}&hash=${expectedHash}`);
  const { verified } = await response.json();
  return verified;
}

使用流式计算避免内存问题

typescript
// 使用 Web Streams API 流式计算 hash
async function calculateHashStreaming(file: File): Promise<string> {
  const reader = file.stream().getReader();
  const chunks: Uint8Array[] = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
  }

  const combined = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
  let offset = 0;
  chunks.forEach((c) => {
    combined.set(c, offset);
    offset += c.length;
  });

  const hashBuffer = await crypto.subtle.digest('SHA-256', combined);
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

AI 协作指南

  • 核心意图:让 AI 帮你实现文件完整性校验。
  • 需求定义公式"请帮我实现分片上传的完整性校验,包括分片级 SHA-256 校验和最终文件校验。"
  • 关键术语SHA-256MD5hash完整性校验 (integrity check)

避坑指南

  • 性能考虑:大文件 hash 计算很耗时,可以使用 Web Worker 避免阻塞 UI。
  • MD5 vs SHA-256:MD5 更快但有碰撞风险,重要数据用 SHA-256。
  • 内存管理:不要一次性加载整个文件到内存,使用流式处理。