1. 为什么“分块上传”必须和“加密存储”绑在一起
做Java后端的人迟早要碰大文件上传。早些年大家习惯直接multipart/form-data一把梭,文件稍微大点就超时、内存爆掉、连接断开重来。后来分块上传成了标配,把一个大文件切成若干块,前端一块一块传,后端一块一块收,最后再合并。但搞定“能传”只是第一步,真正让项目上线后睡不着觉的,往往是一句话:文件落盘之后,别人能不能直接拿走用?
这个标题提的是“加密存储在JAVA分块上传中的技术探讨”,说白了就是解决两件事:第一,分块上传这块骨头怎么啃;第二,啃下来的肉(文件数据)怎么安全地存下来。二者一旦结合,就不是简单的“传完再加密”这么直白。分块意味着文件在网络上被拆散传输,在服务器上是分片落盘,每一片都可能被单独读取;存储加密意味着要保证每个分片、以及最终合并出来的完整文件,都不能被未授权的人解密。这里涉及到传输通道、分片落盘、密钥管理、解密合并四个环节,任何一个环节漏了,前面做得再好也是白搭。
这篇文章面向的读者,我默认是已经在用Spring Boot做接口对接、已经在处理文件上传但还没系统考虑过安全的Java开发者。全文会围绕“加解密如何嵌入分块生命周期”这条主线展开,从方案选型、代码实现到常用踩坑排查,给出一套能直接复用的工程实践。
先说结论:分块上传 + 加密存储的组合,真正要命的不是算法本身,而是分块的边界和加密的粒度之间的关系。选错加密模式,合并出来的文件可能只有第一块能解开,后面全是乱码。这个坑我见过不止一次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方案选型:加密算法和分块流程怎么匹配
2.1 三层加密设计,别只盯着算法
很多人的第一反应是“用AES加密一下不就完了”。没错,AES肯定是要用的,但放在分块上传的语境里,光选AES还不够,你得想清楚加密发生在哪一层。
我习惯把加密拆成三层来看:
第一层是传输层加密,也就是HTTPS。这一层保护的是数据在网络上传输时不被窃听,但注意,它保护不到服务器端的落盘文件。你总不能指望TLS在上传结束后还帮你守文件。
第二层是应用层分块加密,也就是在Java后端收到每一个分块后,对它单独做加密再落盘。这一层是本文讨论的重点,因为它决定了每个分片在服务器上是不是“密文形态”。
第三层是静态存储加密,比如把整个磁盘或对象存储桶做透明加密。这一层通常是基础设施的事,但如果你用的是自建服务器,千万别指望系统自带的透明加密能覆盖所有场景。
真正需要你亲手写的,是第二层。但在写第二层之前,必须先设计好分块和加密的对应关系。
2.2 分组模式选型:CBC、GCM还是CTR
我这里直接给结论:分块上传场景,不推荐用CBC模式做分块独立的加密。
原因很简单:CBC模式是链式的,前一个密文块会作为下一个明文块的加密输入。如果按分块独立加密,每一块都需要随机IV,那合并后的文件其实是一个个“独立加密单元”拼起来的。解密的时候,每一块必须用当时加密用的IV才能解开;而且CBC的padding处理在流式场景非常容易踩坑,一旦某一块长度不对,解密直接失败。
GCM模式是AEAD加密,自带认证和完整性校验,听起来很适合。但它的坑在于:GCM的认证标签(tag)是针对整个密文串计算的,如果你对每个分块单独做GCM,那么合并后的文件没法直接整体验签,只能分块验。如果前端传过来的分块顺序是乱的,合并时对不上tag,解密直接炸。
真正适合分块上传+合并场景的,是CTR模式或着CTR模式的可认证变体。CTR模式本质是把AES变成一个流密码,加密和解密都是对计数器值做加密,再跟明文异或。它的最大好处有两个:一是密文长度和明文长度完全一致,不需要处理padding;二是每个分块的加密位置可以通过计数器精确控制,合并的时候只要保证计数器连续,整个文件能无缝解出来。
我用的是AES/CTR/NoPadding,配合一个全局唯一的初始向量(IV)和递增的计数器来划分每个分块的加密位置。这样,分块上传和加密存储就真正变成了同一件事:每个分块就是一个“计数器区间”的异或结果。
2.3 完整性校验:不能只靠加密算法的tag
有人会问:GCM有认证功能,用了CTR之后完整性怎么办?答案是用HMAC-SHA256对整个合并后的文件(或对密文整体)再做一次签名校验。
我实际项目的做法是:分块上传阶段,每一块计算一个SHA-256摘要,记录在分块元数据里,用来保证分块在网络传输中没有损坏;全部上传完成后,服务端合并出完整密文,再用主密钥派生出一个HMAC密钥,对整个密文算一个HMAC-SHA256,存到元数据里。解密时先校验HMAC,再开始解密。这样既享受了CTR模式在分块场景下的灵活性,又弥补了CTR不提供认证的缺陷。
不要觉得多算一次HMAC是浪费。线上环境里,文件被篡改、分块顺序被重放、存储介质静默损坏,这些都是真实会发生的。有HMAC在,至少能在解密之前发现异常,而不是等解密出一堆乱码再去猜原因。
3. 分块上传中的加密流程设计
先不谈具体代码,把完整流程捋一遍。这个流程我直接在项目里落地过,核心原则是:上传会话启动时生成密钥材料,每个分块都基于同一个主密钥+全局IV+计数器偏移来加密,服务器永远不落明文。
3.1 上传会话初始化
前端在开始上传之前,先调一个初始化接口,例如POST /upload/init。这个接口做什么事?
服务端生成一个uploadId,作为整个上传会话的标识;生成一个16字节的随机IV(实际可用8字节随机数+8字节计数器空间,但你用满16字节也安全);同时生成一个随机的数据加密密钥(DEK),注意不要直接用主密钥去加密文件,而是生成一个临时DEK,再用主密钥去加密DEK(也就是常说的信封加密)。
这个DEK可以放在缓存里(Redis),key就是uploadId,value是DEK密文;也可以序列化到数据库字段里。总之,要保证整个上传会话期间,服务端有办法随时拿到能解出DEK的材料,但明文DEK不要落库,只在内存里短暂存在。
初始化接口返回给前端:uploadId、每块的建议大小、IV值、以及总块数的计算方法。
3.2 分块编号与计数器偏移的计算
这一节是整个方案的技术核心。我先给参数,再解释为什么这么算。
我定的分块大小是5MB(当然你可以调成1MB、10MB,逻辑不变)。AES分组的块大小是16字节,CTR模式的计数器也按16字节来算。
设文件总长度为fileLength,分块大小为chunkSize,第i个分块(从0开始)在文件中的起始明文偏移量为offset = i * chunkSize。
CTR模式下,计数器值由IV和块计数器组成。我用的计数器策略是:
- 将16字节IV视作一个128位的大整数:高64位是随机数,低64位是计数器,初始值为0。
- 对文件中的一个分组(16字节)加密时,使用的计数器值 = IV的低64位 + 该分组在明文中的分组序号。
- 分组序号 =
floor(globalOffset / 16),其中globalOffset是该分组在整个文件中的绝对偏移量。
那么,第i个分块在整个文件中的起始分组序号是:
text复制startBlockIndex = offset / 16 = (i * chunk
