不能直接上传带密码保护的PDF,因浏览器无法解密,需前端用用户密码派生AES-GCM密钥加密PDF字节流,并连同IV、salt等元数据上传。

不能直接上传“带密码保护的PDF文件”本身——浏览器不识别PDF密码,input type="file"拿到的只是加密后的二进制流,后端无法自动解密。真正可行的路径是:前端读取文件 → 用户输入密码 → 用密码派生密钥 → 在上传前对PDF内容加密(或解密后重加密)→ 连同IV、盐值等元数据一并提交。
为什么不能直接传密码保护的PDF
用户本地打开的“密码保护PDF”,其密码验证完全由PDF阅读器在客户端完成;input type="file"只暴露File对象,里面没有“密码字段”,也没有解密接口。你拿到的file就是一段被AES-256加密过的字节流(类型仍是application/pdf),后端收到后若无密钥,连是否为合法PDF都校验不了。
常见错误现象:
- 上传后服务端解析失败,报错
Invalid PDF structure或直接返回空响应 - 误以为
accept="application/pdf"能过滤掉非PDF或带密码PDF——它只按MIME类型匹配,加密PDF仍算application/pdf - 试图用
FileReader.readAsText()读取加密PDF,结果是乱码或报错DOMException: The requested file could not be read
Web Crypto API实现前端加密上传
必须用window.crypto.subtle对PDF内容加密后再上传,确保传输和存储时始终是密文。关键点不是“保护PDF”,而是“用用户密码控制访问权”。
立即学习“前端免费学习笔记(深入)”;
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
实操建议:
- 用
FileReader.readAsArrayBuffer(file)读取PDF原始字节,避免转base64(体积膨胀、内存残留风险) - 用
crypto.subtle.importKey()+deriveKey()(PBKDF2 + salt + 100_000次迭代)从用户密码生成AES-GCM密钥 - 加密时生成12字节
iv和认证标签,它们必须和密文一起上传,不能丢 - 用
FormData.append()分字段提交:FormData.append("encrypted", encryptedBlob)、FormData.append("iv", btoa(String.fromCharCode(...iv)))、FormData.append("salt", btoa(String.fromCharCode(...salt))) - fetch发送时**不要设置
Content-Type头**,让浏览器自动生成multipart/form-data; boundary=...
示例片段:
const reader = new FileReader();
reader.onload = async () => {
const arrayBuffer = reader.result;
const password = document.getElementById("pwd").value;
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await crypto.subtle.importKey(
"raw",
await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 100_000, hash: "SHA-256" },
await crypto.subtle.importKey("raw", encoder.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]),
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
),
{ name: "AES-GCM" },
false,
["encrypt"]
);
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, arrayBuffer);
const fd = new FormData();
fd.append("encrypted", new Blob([encrypted], { type: "application/octet-stream" }));
fd.append("iv", btoa(String.fromCharCode(...iv)));
fd.append("salt", btoa(String.fromCharCode(...salt)));
fd.append("filename", file.name);
fetch("/upload-encrypted", { method: "POST", body: fd });
};
后端接收与解密的硬性要求
前端加密只是第一步,后端能否安全解密取决于三个不可妥协的条件:
- 必须通过HTTPS接收请求,否则密钥派生过程中的盐值、IV可能被中间人截获
- 必须严格校验
FormData中是否存在iv、salt、encrypted三者,缺一即拒收(防降级攻击) - 解密失败时只能返回
400 Bad Request,绝不能泄露"IV mismatch"或"Wrong password"等提示 - 解密后的PDF明文必须在内存中处理完立即清零(如Node.js用
buffer.fill(0)),禁止写临时文件或打日志
最容易被忽略的是:PDF加密格式本身(如Adobe的RC4/AES)和前端用Web Crypto做的AES-GCM是两套体系——你不是在“上传一个已加密PDF”,而是在“把PDF当作普通二进制,用另一套密钥重新加密”。这个逻辑混淆会导致整个方案失效。


















