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-256、MD5、hash、完整性校验 (integrity check)
避坑指南
- 性能考虑:大文件 hash 计算很耗时,可以使用 Web Worker 避免阻塞 UI。
- MD5 vs SHA-256:MD5 更快但有碰撞风险,重要数据用 SHA-256。
- 内存管理:不要一次性加载整个文件到内存,使用流式处理。
