资讯动态

从浏览器到数据库:银行级大文件上传加密链路实战

发布时间:2026/10/19 7:14:42 来源:尧图企业网站定制
在银行系统里折腾大文件上传真不是写一个 multipart 接口把文件存起来就完事。客户批量导入的 Excel 可能带着几万条身份证号贷款审批附件里是含住址和收入信息的合同扫描页这些数据一旦以明文形式经过任何一个环节后面审计就能让你连续几周都睡不踏实。我最近在做某银行客户资料导入模块的加密改造核心需求就一句话大文件上传全链路中敏感客户数据不允许出现明文落盘、明文日志、明文缓存。这篇文章把这条从浏览器到数据库的加密链路拆开讲适合正在做 JavaWeb 文件上传、又对数据安全有硬要求的开发者参考。如果你以为“给上传接口加个 Cipher”就够了那我必须直说完全不够。1. 这个需求真正在防什么拆解敏感客户数据的暴露面1.1 大文件上传里最容易被人忽略的三类敏感数据第一类是结构化批量数据典型就是 CSV、Excel。客户经理在网点导出一份名单里面几十个字段姓名、身份证号、手机号、家庭地址、银行卡号全都有单条记录不大但整个文件可能几百 MB。这类数据的问题在于开发人员处理时习惯把整份文件当成普通文档只对数据库字段做加密文件本体却到处乱放。第二类是非结构化影像件。身份证正反面照片、银行卡照片、合同扫描件它们本身不占用太多结构化字段但图片里客户姓名、证件号码、住址都是明晃晃的。更麻烦的是银行风控或贷后审核经常需要人工调阅所以你不能“只提取数据不保留原文件”文件必须整体留档。一旦这些影像件被明文存储权限或备份配置稍有疏漏泄露的就是一整批客户身份信息。第三类是混合型文件比如带了宏和批注的 Excel、带修订记录的 Word、含作者和打印机路径的 PDF。表面看文件名只是一个业务附件但文件内部隐藏的元数据可能残留上一位客户的信息。我在实际项目里甚至遇到过上传模板里宏代码能读取本机用户名的情况虽然业务系统不一定会执行宏但这类文件如果直接暴露在下载环节也会带来额外风险。所以判断一个文件是否属于“敏感客户数据”不能只看后缀名和业务字段还要看文件内容、文件元数据以及使用场景。只有把这三类都纳入加密范围才算把口子堵住。1.2 从浏览器到数据库一条文件要经过多少个明文节点把一次大文件上传完整拉通来看明文出现的位置远比你想的多。浏览器先把文件发送到负载均衡或网关这一段默认是 HTTPS但 HTTPS 只保护传输过程中不被第三方窃听服务端接收后数据在应用内存里就是明文。很多网关和反向代理为了排障会把请求 URL、上传文件名、客户端 IP 记到访问日志里如果文件名本身是“张三_身份证.pdf”日志这一层就已经泄了。到了应用服务器更常见的问题是 Spring Boot 默认的 MultipartFile 处理逻辑。如果你直接调用 transferTo()框架会把上传内容先写进操作系统的临时目录这个临时文件通常是明文而且不会立刻自动删除。期间应用如果被导出线程 dump、发生内存溢出后留下堆转储文件JVM 堆里正在处理的明文内容也可能被带出去。再往后是转存和落库。对象存储如果没开服务端加密文件对象本身就是明文数据库里的 BLOB 字段如果依赖表空间加密而备份系统只是复制物理文件那备份出来的数据同样容易读。还有一个经常被忽略的地方是调用链日志调用远程服务时如果框架自动打印请求体 JSON某个字段里夹着客户手机号或身份证号也会变成日志明文。我总结了一个原则不要把“加密”理解为某一次密码学操作而是把明文窗口尽量收窄到“业务真正需要处理的那一瞬间”。比如解析 Excel 提取客户信息时页面内存里出现明文是允许的但落盘、入日志、进中间件缓存这些环节应该尽量让明文不出现。2. 加密方案选型为什么“上传后再加密”不是简单加个 Cipher2.1 传输层加密与应用层加密各管哪一段很多人一上来就提“用 HTTPS 不就行了”这是混淆了两个层次。TLS/SSL 解决的是浏览器和服务器之间链路上的窃听与篡改一旦数据到达服务器网关和业务进程对数据就是透明的。也就是说HTTPS 再强也管不了服务器本地磁盘、运维人员、备份磁带、对象存储共享账号这些后续环节。银行类系统通常有两道明确要求传输层必须启用 TLS这属于默认值应用层还必须对敏感数据本体做加密防止数据在存储和内部流转时被盗。应用层加密的位置越早越好但不必激进到在浏览器端用 JavaScript 那把文件整套加密。浏览器端做加密最大的问题是密钥怎么分发你总不能让每个客户的浏览器都保存一把业务系统私钥而且前端加密逻辑一旦被调试改写保护意义会大打折扣。实际工程里最稳妥的做法是“HTTPS 后端流式加密 存储加密 数据库字段加密”组合。传输层管链路应用层管静态数据两层叠加而不是二选一。2.2 AES-GCM 还是国密 SM4流式加密的算法选择做文件加密首先要选对称加密算法因为大文件动辄几百 MB、几个 GB用 RSA 这类非对称算法硬扛性能不可接受。非对称加密只适合做密钥信封比如后面讲的 KEK 包装 DEK不适合直接加密文件内容。在对称算法里主流选择是 AES-256-GCM或者合规要求明确指定国密时用 SM4。AES-256-GCM 在 Java 标准库原生支持不需要额外引入第三方包而且现代 CPU 基本都有 AES-NI 指令集流式加解密吞吐量很高。它自带认证标签加密的同时能校验数据是否被篡改这正是文件上传场景最需要的——你总不想把被中间人换过的“密文”当成客户资料存下来。SM4 是国密标准如果业务方明确要求信创或国密合规那就要替换成 SM4-GCM 或者 SM4-CTR 加 HMAC。替换并不难难的是元数据设计一开始就要留好算法标识字段否则老文件用 AES、新文件用 SM4解密端会乱套。我用一张表概括几种模式的差别算法模式完整性校验流式支持需要额外维护 MAC适用场景AES-256-GCM自带认证标签支持否通用推荐默认选择AES-CTR HMAC-SHA256需要额外 HMAC支持是需要分段认证、自行控制时SM4-GCM自带认证标签支持否国密合规的通用选择SM4-CTR HMAC需要额外 HMAC支持是国密合规且需要分片认证时2.3 整文件单流加密与分片独立加密的取舍加密方式上一般有两种路线整文件单流加密和分片独立加密。整文件单流加密最简单。文件开头写入 IV 和算法标识然后用一个 CipherOutputStream 从头写到尾整个文件只有一个认证标签。解密时也顺畅读文件头、初始化 Cipher、流式解密成一个明文文件。缺点是上传中断后已经写进去的密文没法单独校验重传基本只能整体重来。如果采用“前端顺序上传分片、后端追加到同一个 CipherOutputStream”的方式还能兼顾分片传输但要求前端必须严格按序号串行上传不能并发。分片独立加密则是把文件切成若干 chunk每个 chunk 用自己的 IV 和认证标签分别加密。每个分片可以乱序传输、独立校验、失败后只重传缺失分片特别适合银行这种网络环境复杂、文件体积动辄几个 GB 的场景。缺点也明显最终得到的不是一个标准 GCM 密文文件而是一组密文块外加一份索引元数据将来如果有人直接把这个文件拷走不知道元数据格式的人根本无法解密。而且分片越多元数据管理、完整性校验的复杂度越高。我的建议是如果文件通常在 1 GB 以内且上传链路很稳定选单流加密加顺序分片如果要做断点续传和并发上传尤其是几百 MB 到几个 GB 的批处理文件直接上分片独立加密。后面实战部分我两种方案都会给出代码骨架大家按场景取用。3. 可落地的 JavaWeb 大文件分片加密上传实战3.1 前端分片与哈希校验别把整文件一次性交给 Servlet先说前端。大文件上传最忌讳 File 对象一次 POST 给后端Servlet 容器默认对请求体大小有限制而且一个几十 MB 的 byte[] 会瞬间压满内存。所以第一步就是分片。前端按固定大小切片不用在意文件格式直接用 File.prototype.slice()。每片作为一个独立的二进制块上传同时带上 fileId 和 chunkIndex。fileId 由前端生成一个 UUID后端把它作为幂等键和元数据主键。比较关键的是哈希校验。每个分片在发送前计算一个 SHA-256 值放在请求头里后端收到后先校验再进入加密流程。为什么要在加密前校验因为网络传输可能丢位或损坏如果先把损坏数据加密成密文后面审计时很难定位是网络问题还是加密问题。校验在加密之前做能把这个不确定性提前排除。对整份文件我建议前端在开始上传前先读一遍文件计算完整的 SHA-256把这个值随第一个分片请求带给后端。这个操作会让用户多等几秒但对于银行客户资料类文件完整性是硬要求值得花这个代价。3.2 后端接收分片时如何直接写密文绕开明文临时文件后端处理分片时最容易被坑的是为了校验或组装先把收到的 chunk 写进一个临时文件等全部收齐再统一加密。这个临时文件就是明文一旦服务器磁盘被拖走数据全裸。正确姿势是分片校验通过后立刻把明文块用 Cipher 加密成密文块然后直接写入对象存储或持久化存储不落任何明文临时文件。如果业务必须要暂存那也是写入“.enc”后缀的密文临时文件而不是明文文件。另外要注意多个后端实例并行时不能依赖本地磁盘作为分片暂存区。因为同一个 fileId 的不同 chunk 可能被负载均衡分发到不同节点本地磁盘会互相看不到。必须用共享存储比如对象存储本身或者分布式文件系统。元数据库记录每个 chunk 的存储位置、IV、长度这样任意节点都可以继续接收后续分片。上传完成不代表可以直接拼接解密。如果采用分片独立加密后端把所有 chunk 的密文块按顺序在解密时重组即可不需要把密文物理拼成一个大文件。如果业务方要求下载时拿到一个标准密文文件那就在元数据里维护一个“密文块索引”解密服务按索引遍历。3.3 完整代码骨架AES-GCM 流式加密/解密 元数据记录先看分片独立加密的核心代码。我这里假设密钥已经从 KMS 获取DEK 只存在于内存里不会写进磁盘。public byte[] encryptChunk(byte[] plainChunk, UUID fileId, int chunkIndex, SecretKey key) throws Exception { byte[] iv new byte[12]; SecureRandom.getInstanceStrong().nextBytes(iv); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); // AAD 绑定文件 ID 和分片序号防止密文分片被拼接到别的文件 ByteBuffer aad ByteBuffer.allocate(20) .putLong(fileId.getMostSignificantBits()) .putLong(fileId.getLeastSignificantBits()) .putInt(chunkIndex); cipher.updateAAD(aad.array()); // doFinal 的输出是“密文 16 字节认证标签” byte[] ciphertext cipher.doFinal(plainChunk); ByteBuffer out ByteBuffer.allocate(12 ciphertext.length); out.put(iv); out.put(ciphertext); return out.array(); }这里的重点是 IV。GCM 模式对 IV 唯一性要求极高同一把密钥下绝对不允许出现两次相同的 IV否则认证标签直接失效。用 SecureRandom 生成 12 字节随机 IV 是常用做法工程上还要加一层检测写入元数据前判断数据库中是否已经存在完全相同的 IV一旦发现就重新生成。再看单流加密方式适合顺序分片或非分片上传try (InputStream in multipartFile.getInputStream(); FileOutputStream fileOut new FileOutputStream(encFile); CipherOutputStream cos new CipherOutputStream(fileOut, cipher)) { byte[] buffer new byte[64 * 1024]; int len; while ((len in.read(buffer)) ! -1) { cos.write(buffer, 0, len); } }这个小代码里有三个容易被忽略的点。第一必须先声 cam 后关 cos因为 GCM 的认证标签是在 CipherOutputStream 执行 close() 时才写入底层流的如果先关 fileOut标签就丢了。第二缓冲区不是越大越好后面调优部分细说。第三不要在这一层再套一层 BufferedOutputStreamCipherOutputStream 内部已经做了必要的缓冲重复包装只会增加内存拷贝。解密时如果是单流用 CipherInputStream 读取整个文件如果是分片独立加密就按索引逐块 doFinal再把明文块写到同一个输出流。分片方式每个 chunk 都是独立认证的解到任意一块时如果认证不过可以快速定位是哪一片出了问题这就是分片方案在运维上的最大优势。元数据表建议这样设计CREATE TABLE file_encrypt_meta ( file_id UUID PRIMARY KEY, algorithm VARCHAR(20) NOT NULL, key_version INTEGER NOT NULL, plain_sha256 CHAR(64) NOT NULL, chunk_count INTEGER NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP NOT NULL ); CREATE TABLE file_chunk_meta ( file_id UUID NOT NULL, chunk_index INTEGER NOT NULL, iv BYTEA NOT NULL, encrypted_size INTEGER NOT NULL, storage_path VARCHAR(512) NOT NULL, PRIMARY KEY (file_id, chunk_index) );注意单流方式不需要 chunk 表IV 直接放在 file_encrypt_meta 里即可。分片方式则必须把每个分片的位置、IV、长度都记录下来否则解密索引不完整。4. 密钥从哪里来、存到哪里去银行场景下的密钥管理4.1 DEK/KEK 两层结构和 HSM/KMS 接入文件内容加密用的对称密钥如果写死在 application.yml 里那不管加密算法多好都等于把家门钥匙放在门口的脚垫下面。银行场景下的标准做法是信封加密也就是密钥分两层数据加密密钥 DEK 和密钥加密密钥 KEK。KEK 保存在 HSM 或集中 KMS 中应用无法直接读取 KEK 明文。实际加密文件之前先随机生成一个 DEK用 DEK 加密文件内容然后用 KEK 加密 DEK得到加密后的 DEK 密文这个 DEK 密文可以安全存放在业务数据库里。解密文件时先从元数据里取出加密的 DEK调用 KMS 用 KEK 解密得到 DEK 明文再用 DEK 去解文件内容。这样做最大的好处是 KEK 轮换时不需要重新加密任何历史文件。比如某天 KMS 要求根密钥更新我们只需要把数据库里所有被旧 KEK 包住的 DEK 密文用新 KEK 再重新加密一遍文件内容完全不用动。DEK 本身可以绑定单个文件某个文件泄露了就只换那一个文件的 DEK影响面小得多。代码层面获取密钥大概是这样的流程String encryptedDekBase64 metaStore.getEncryptedDek(fileId); byte[] dek kmsClient.decrypt(encryptedDekBase64, KEY_USAGE_FILE_ENCRYPT); SecretKeySpec key new SecretKeySpec(dek, AES);DEK 用完之后要把内存里的 byte[] 清空不能留着等 GC。真实 Java 环境里可以用 try-finally 里的 Arrays.fill(dek, (byte) 0) 做一次主动清理虽然不完美但能降低堆转储泄露的概率。4.2 密钥轮换时老文件怎么平滑过渡很多人以为密钥轮换就等于把所有历史文件重新加密一遍这是最吓人的运维噩梦。其实有了信封加密日常轮换根本不需要碰文件本体。轮换 KEK 时只需要启动一个后台任务遍历所有文件元数据用旧 KEK 解出 DEK 密文再用新 KEK 加密一次更新元数据里的 key_version 和加密后的 DEK 密文。这个操作只涉及数据库里的小数据几分钟就能完成。轮换 DEK 才是真正的大工程因为意味着文件内容要重新加密。一般只在发现某文件 DEK 泄露、或者文件级别安全要求强制时才做。做法是后台分批读取密文文件、解密再重新加密过程中要保证文件状态是“可读旧版本”或“迁移中”不要让用户看到一半新一半旧。实际银行项目里我倾向于把 DEK 有效期设计得很长并依靠 key_version 字段做版本切换除非有明确事件否则不主动批量重加密文件。解密端必须把 key_version 作为解密参数的一部分。因为历史文件里的 DEK 是用旧 KEK 包的解密时必须根据 key_version 找到对应 KEK不能默认用当前 KEK 去解否则老文件全部解不开。4.3 加密文件的元数据如何安全落库即使文件内容是密文元数据本身也可能泄露敏感信息。比如对象存储里的文件名如果叫“张三_身份证.pdf.enc”那就算文件内容加密了别人扫一眼文件列表照样知道张三传了身份证。所以对象存储的 key 必须用无业务含义的随机标识我通常采用{tenantId}/{yyyyMM}/{fileId}.enc这样的结构客户 ID 只存在于业务数据库中不直接暴露在存储层。元数据表里的 customer_id、业务类型这些字段能脱敏的先脱敏不能脱敏的考虑字段级加密。字段级加密和文件级加密最好使用不同的 DEK避免同一个密钥被用于多个场景后一处泄露波及所有数据。我见过一个真实踩坑场景开发人员图省事把客户手机号直接作为对象存储 key 的一部分结果对象存储的访问日志里全是手机号这等于在物理层之外又增加了一个明文出口。所以加密方案里一定要有一条硬性要求所有外部系统能看到的数据都必须是无法直接还原出客户信息的随机标识。5. 实测表现和调优坏味道往往藏在吞吐量数字背后5.1 缓冲区和流式处理对大文件吞吐量的影响大文件加解密最容易出现的性能问题是缓冲区太小导致系统调用频繁。在模拟项目 X 里我们做过一个简单对比8 KB 缓冲区时加解密 200 MB 文件耗时明显偏高改成 64 KB 后吞吐量几乎翻倍再往上加到 1 MB 提升就很小了。原因是 AES-GCM 本身有 AES-NI 硬件加速CPU 计算不是瓶颈瓶颈大多在内存拷贝和操作系统读写次数上。所以 64 KB 是一个比较通用的起点。如果你用的是 JDK 11 以上可以在测试环境跑一次 jmh分别测 16 KB、64 KB、256 KB找到自己机器上的拐点。但注意不要把缓冲区设成十几 MB大缓冲区会让年轻代 GC 压力上升尤其在高并发时反而影响整体吞吐。另一个细节是 CipherOutputStream 的 flush。加密流里的 flush 不见得会立即把数据写到底层因为 GCM 为了保证认证标签内部是有缓冲的。不要为了“实时落盘”频繁调用 flush正确的策略是写满一个缓冲块后再由写操作自然触发最后靠 close 把数据和 tag 一起写出去。5.2 防止上传超时与失败重传的工程化设计银行的文件上传服务经常会挂在网关层长时间占用连接容易被裁掉。所以必须把“一个大文件上传”拆成“很多个小分片上传”网关超时时间只需覆盖单个分片即可。分片大小建议在 10 MB 到 50 MB 之间。太小编程开销太大太大则失去断点续传意义。如果网络质量差可以把分片调小到 5 MB让失败重传的单位更小。服务端需要记录每个分片的接收状态可以用 Redis 的 bitmap 存upload:{fileId}:chunks每接收一片就把对应位置置 1。前端重试时先问一下后端“哪些分片还没收到”只传缺失部分不要重新传整个文件。后端接口必须是幂等的。同一个 fileId、同一个 chunkIndex 重复提交时如果该分片已经存在且哈希一致直接返回成功不要重复执行加密和写入。这样网络重试造成的重复请求才不会把存储写乱。5.3 典型压测结果与 JVM 参数调整在模拟项目 X 里我们用 4 核 8G 的物理机模拟过并发场景同时开 10 个 200 MB 文件上传采用分片独立加密每片 10 MB后端加密后写入对象存储。最终瓶颈不是 CPU 也不是内存而是对象存储的写入吞吐量。加解密本身大约只占 CPU 的 20% 左右剩余开销大多在 TLS 握手和对象存储客户端序列化上。因此压测时不要只盯着加解密代码要多看网络连接池、TLS 会话复用、对象存储客户端的最大连接数。JVM 堆设置反而简单因为全程流式处理堆里不会同时积压整个文件Xmx 给 2 GB 足够应付常规并发。真正要留意的是元数据写库的并发和文件状态位图的一致性这才是性能拐点。6. 从开发到生产我在银行环境里踩过的那些坑6.1 日志和异常信息无意间把明文带上来了开发环境里大家习惯直接打印“上传成功filename测试.xlsxcustomer_name张三”看起来没什么。但生产环境日志是集中采集的会进 ELK会被多个团队检索权限管理往往比业务系统松得多。客户姓名、手机号一旦进入日志后面想删都删不干净。后来我们统一做了日志脱敏过滤器对身份证号、手机号、银行卡号做正则替换。更严格的是业务代码里禁止把整个客户对象直接 toString() 打印异常信息里也只用 fileId 和异常类型不携带业务字段。这是银行环境审计的基本要求也是我每次 code review 一定会盯的点。6.2 IV 复用的“隐形地雷”GCM 模式最忌讳的重复随机数有一类问题是平时测不出来、出事就要命的GCM 模式下如果两次加密用了同一个 IV 和同一把密钥攻击者把两块密文做异或就能直接拿到明文。所以代码里绝不能写死 IV也不能用时间戳的前 12 字节当 IV更不能用 UUID 截断当前缀。正确做法是用 SecureRandom 生成 12 字节随机 IV同时在写入元数据库前检查唯一性。还有一个改进用 fileId 和 chunkIndex 派生 IV这样同一个文件的不同分片天然不会重复不同文件因 fileId 不同也不会重复。但派生方式要稳定别在代码升级后改变规则否则老数据全部解不开。6.3 加解密完整性校验与“解密成功但数据被改过”的边界AES-GCM 自带认证理论上密文被篡改时解密会直接报错。但分片独立加密有一个整体性问题每一片都能独立认证不代表整个文件没被换过分片序。比如攻击者把文件 A 的第 2 片和文件 B 的第 2 片调换单片认证都能通过但拼接出来的明文文件已经不是原始内容。解决办法就是 AAD 绑定 fileId 和 chunkIndex我前面代码里已经示范了。这样每一片的认证标签都绑定文件身份和分片位置任意调换都会导致认证失败。另外还要在最后做一次明文 SHA-256 比对前端上传前算的原始哈希、后端解密后重新算的哈希必须一致才能最终确认文件没被掉包。6.4 上线前的两类必测场景超大文件与并发上传我每次改这类功能都会强制要求测试环境跑两个场景。第一个是超过 2 GB 的超大文件专门检查分片编号会不会超过 int内存有没有被某个 byte[] 撑爆网关和对象存储对超大单流的限制是否开启。第二个是 20 个并发上传同时进行检查元数据表有没有写锁竞争、Redis 分片状态位图有没有交集、KMS 限流会不会把整个服务打挂。这两类场景过了基本能覆盖生产环境九成的问题。我的个人习惯是在上线前放一个巡检脚本跑一遍临时目录、访问日志、对象存储列表用正则扫“身份证号、手机号、.xlsx、.pdf”这些关键词一旦出现明文残留立刻报警。这个习惯让我在好几个项目里都提前发现了别人没看到的明文泄露点也算是我最想分享给同行的一条实战经验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑