但凡做过文件上传功能的开发者,大概率都被告知过一条铁律:加上校验和,保证数据完整性。但很少有人会追问一句——这个“完整性”到底保的是什么,不保的又是什么。一份技术文档把这个问题掰开揉碎了讲清楚:校验和能证明的,比你想象的要少得多。
先说结论。校验和,或者说摘要,能做两件事:检测文件在传输过程中是否意外损坏,以及判断两段字节序列是否一模一样。仅此而已。它不能证明这个文件是谁创建的,不能证明上传行为有没有获得授权,更不能证明这个文件本身是否安全、能不能放心展示。把校验和等同于“安全”,是上传协议设计里最常见也最危险的误解。
先搞清楚,你到底在算什么
第一个需要明确的问题听起来简单,实际操作中却很容易被忽略:你到底对哪一段字节做了哈希?用户在浏览器里选中的原始文件、分片上传时的每一个片段、服务端组装好的完整对象、以及转码后生成的衍生文件,这四样东西对应的是完全不同的字节序列。系统如果需要在多个边界上做验证,就必须分别记录各自的摘要,不能混用。
对于承诺保留原始文件的服务来说,最有价值的对比,是浏览器端原始文件的摘要和服务端组装后存储对象的摘要。这两者对上,才意味着用户上传的东西和服务器存下来的东西是同一份。大文件处理上有个细节值得留意——不要因为图省事就把整个文件读进一个超大的 ArrayBuffer,流式上传是更合理的选择。
浏览器有浏览器的脾气
标准 Web Crypto 的 digest API 需要你把完整的输入缓冲区传给它。照片之类的小文件这么做问题不大,几行代码就能搞定:拿到文件的 ArrayBuffer,丢进 crypto.subtle.digest 算一个 SHA-256 出来。但换成视频,内存压力马上就上来了——主线程直接卡死,用户只能盯着一个“正在准备”的转圈动画干等。
做增量哈希需要引入经过仔细审查的第三方库,或者用 Worker 把计算丢到后台线程去执行。技术文档里特地强调了一个点:不要在浏览器主线程上做耗时操作,更不要拿“加载中”的提示来糊弄用户。用户感知到的卡顿,比你以为的更影响体验。
部分摘要拼起来不是整体摘要
这也是个高频踩坑点。把每个分片的十六进制摘要字符串拼接起来,然后以为这就是整个文件的 SHA-256——不是的,完全不是。分片摘要和整体摘要之间没有这种简单的代数关系。同理,对象存储返回的 multipart ETag 可能用的是厂商自己的算法,不能想当然地当成文件完整性凭证。
那部分哈希到底有什么用?如果上传协议对每一片都做了编号验证,并且服务端按照可信顺序把分片组装起来,那么分片摘要保的是传输过程的完整性——每一片在路上没坏,到了地方也没装错顺序。至于文件从服务器导出或者跨系统流转时的验证,需要另外一份独立的整体摘要来兜底。
把元数据和摘要绑在一起
单独一个摘要值本身,没有任何上下文信息。它不告诉你这是什么时候算的、对应哪个文件、用的是哪种算法。所以存储摘要的时候,必须同时记录上传意图和不可变的字节长度。算法版本号也不能省,否则将来系统做算法迁移,旧数据会被错误地重新解读。
还有一个安全实践容易被忽视:不要把原始摘要直接当作私有文件的公开对象标识来用。如果你这么干了,任何一个手里有常见文件的人,都可以通过计算摘要来试探这个文件是否存在于你的系统里。对象标识应该用随机值,权限验证必须独立做。
校验失败怎么处理才不踩坑
摘要匹配失败时,系统应该拒绝接受这份文件,然后触发有边界的重试或直接重新开始。同时保留安全的原因码和足够的分片级证据,方便排查是网络问题还是实现层面的 bug。文档里特别指出了两个禁止行为:不要把文件内容写进日志,不要把签名过的 URL 写进日志。
如果一个特定客户端版本反复出现校验失败,先别急着认定是恶意流量。更大概率是那个版本本身存在哈希计算上的 bug。排查对比的时候,把浏览器大版本和应用版本放在一起看,往往比盯着单次请求的日志更有效。
去重这件事,想清楚再做
服务端去重确实能省存储空间,但跨事件的摘要相等会悄悄泄露信息,还会让数据删除变得异常复杂——你删了一条媒体记录,但相同的字节可能还被别的记录引用着,删还是不删?更稳妥的做法是,只在同一个事件或同一个所有者范围内做去重。引用计数要维护准确,确保删除操作的语义不会跑偏。
文档最后提到一个点,很容易被扩展成另一场讨论:感知哈希回答的是完全不同的问题,而且会带来额外的隐私风险。在还没有想清楚这些风险之前,不要轻易把它引入上传流程。校验和是工具,不是信仰。知道它能证明什么、不能证明什么,比用它本身更重要。
热门跟贴