在军工背景的项目里待久了你会发现大文件上传这类“听起来很普通”的功能一到真实交付场景就变得特别棘手。十几G的测绘数据、试验记录、设计图包既要内网环境下稳定传完还得保证传输过程中没人动过手脚、断了能续传、传完能审计。标题里的这个“安全”两个字绝对不只是“HTTPS加密”那么轻巧。这篇文章我想把之前在ASP.NET技术栈下做断点续传组件安全性设计的思路完整梳理一遍。先说结论断点续传这件事本身不复杂复杂的是在“客户端不可信、链路不可信、审计要闭环”的前提下把续传功能做成一个能扛住真实攻击面、能通过安全评审的组件。整套设计里有七个环节我建议任何一个高安全场景下的上传组件都必须认真对待下面逐个展开。1. 军工场景下的上传链路到底在防什么1.1 为什么大文件在这个场景里尤其危险很多从互联网产品转过来的同事第一反应是大文件上传不就是分片、并发、断点续传吗把前端切片调好、后端合并写完、进度条不卡就完事了。但在涉密内网、军工交付这类场景里文件的敏感级别决定了整个功能的设计权重。大文件的危险点在于它“暴露时间长”。一个小文件几秒钟传完攻击窗口很短一个几GB的文件可能传上几分钟甚至几十分钟这个过程中间有大量的临时文件、分片请求、状态记录。任何一个环节如果出现越权访问、篡改注入、残留数据都可能直接把敏感文件内容泄露出去。另外大文件传输对服务端的资源消耗大容易成为拒绝服务攻击的突破口。一个恶意客户端可以不传真实文件只发一堆伪造的分片请求把服务器的磁盘和内存打满。所以在军工场景里上传组件首先要回答的问题不是“能不能断点续传”而是“在不可信的网络和客户端环境下怎么保证断点续传的每一个环节都被验证、被记录、被隔离”。1.2 常规断点续传方案在这里会栽的三个跟头市面上的大文件上传组件很多但直接拿来用在军工背景项目里通常会暴露这些问题第一个是信任前端参数。很多组件把当前上传位置offset、分片序号、文件总大小这些参数完全交给前端客户端传过来服务端只是被动接收并写入。这在公网产品里勉强能跑但在高安全场景里就是漏洞。客户端被改造后可以伪造offset把任意分片写到文件任意位置甚至可以把一整个恶意文件的分片“续传”到已通过校验的正常文件上。第二个是临时文件权限失控。默认配置下临时分片目录如果落在Web站点目录或者一个共享可读的路径下分片文件在传输过程中就等于裸奔。更常见的情况是开发阶段为了方便测试把临时目录权限设成Everyone可写上了生产环境也没收敛。第三个是审计不足。普通组件顶多记录“文件上传成功”或者“上传失败”但完整的分片级审计、校验失败审计、删除审计几乎没有。一旦出了安全事件你根本还原不了当时发生了什么。所以这里最重要的设计原则是服务端只把客户端发来的数据当作“待验证的输入”一切以服务端自己的记录和服务端实际接收到的字节为准。2. 断点续传的协议设计先定规则再谈实现2.1 分片协议与元数据设计我的习惯是先定协议不急着写代码。组件化开发最怕的是规则没定好后面每个版本都在打补丁。分片大小建议按网络环境来内网千兆可以选8MB到32MB广域网建议4MB到8MB。分片太大断点续传的粒度太粗重传代价高分片太小HTTP请求数量暴增服务端和数据库的压力都上去了。我之前在一个项目里用4MB分片一个1GB的文件就是256片对于关系型数据库存分片记录来说完全没压力。初始化上传时客户端先向服务端申请一个上传会话服务端返回uploadId。这个uploadId是后续所有操作的凭证。元数据记录这样设计字段说明uploadId服务端生成的全局唯一标识fileName原始文件名服务端只存脱敏后的存储名totalSize文件总字节数chunkSize约定分片大小totalChunks总分片数receivedChunks已成功接收的分片索引集合fileHash整文件SHA-256客户端最后上传status初始化/传输中/已完成/已过期有一个细节fileName如果直接信任客户端传过来的字符串会有路径穿越和非法字符注入风险。我一般是服务端重新生成GUID文件名原始文件名单独存数据库磁盘上永远不出现原始文件名。2.2 续传断点的判定权必须握在服务端手里断点续传的经典实现是客户端问服务端“我从哪接着传”服务端查询uploadId的receivedChunks集合返回缺失分片列表。客户端的任务只有一个就是把缺失的分片按照编号重新传一遍。这个逻辑看起来简单但有个关键点服务端绝不能接受客户端自己声明的“偏移量”作为写入位置。每个分片的写入偏移必须由服务端根据分片索引和约定分片大小计算出来。比如chunkSize4MB服务端接收index5的分片时写入位置就是 5 * 4 * 1024 * 1024不是客户端传一个offset2097152服务端就信。这样设计之后即使客户端伪造index也不能把文件写到预期之外的偏移位置。下面是一段服务端Controller的要点示意[HttpPost(upload/init)] public async TaskIActionResult InitUpload([FromBody] InitUploadRequest req) { // 校验文件大小、扩展名白名单、剩余存储空间 // 生成 uploadId创建元数据记录 // 返回 uploadId、chunkSize、totalChunks } [HttpPost(upload/chunk)] public async TaskIActionResult UploadChunk( [FromQuery] string uploadId, [FromQuery] int chunkIndex, IFormFile file) { // 1. 根据 uploadId 加载元数据 // 2. 校验 chunkIndex 在 [0, totalChunks-1] 范围内 // 3. 校验分片实际大小除最后一片外都必须等于 chunkSize // 4. 计算该分片服务端接收内容的 SHA-256 // 5. 只有哈希通过后才标记 receivedChunks[chunkIndex] true }这里把分片写入位置的计算逻辑放在服务端内部而不是依赖客户端传入的offset参数。即使客户端被恶意改造也无法把分片写到计划外的字节位置。2.3 为什么不能直接相信前端汇报的文件信息很多浏览器端的断点续传组件会先在前端用File API读取文件基本信息然后汇报给服务端。但这些信息本质上都是不可信的客户端可以声明任意文件大小、任意文件类型。所以在服务端初始化上传时不要一次性信任客户端的totalSize。可以在全部接收完成、合并校验阶段以服务端实际接收到的总字节数、全文件SHA-256为准。如果客户端声明的大小与实际接收不一致直接判定上传失败并清除所有临时数据。补充一点军工项目里经常有“秒传”需求意思是同名同哈希文件不需要重新上传。这个功能要在文件哈希比对通过后才能启用绝不是前端说“这个文件上传过”就直接跳过。3. 完整性校验链从分片到整文件逐级兜底3.1 三级校验体系只做整文件哈希校验是不够的原因很简单等你发现整文件哈希不对的时候可能已经传了半小时你得让客户端重新传全部数据这个体验在军工场景里根本没有接受的余地。我惯用的方案是三级校验第一级分片级校验。每个分片到达服务端后立刻计算SHA-256与客户端上报的分片哈希比对不一致就直接拒绝。这一级能拦截绝大部分网络传输损坏和恶意篡改。第二级分段校验。每隔固定数量的分片比如16个分片64MB为一组对这一段已接收的数据算一个分段哈希。分段哈希存在的意义是防止服务端合并逻辑被绕过同时也为日志审计提供更多锚点。第三级整文件校验。所有分片接收完成后在合并过程中边写边计算全文件SHA-256最后与客户端上传的fileHash比对。这里有个原则要知道客户端上传的fileHash只能作为参考如果和合并结果不一致可以直接判定失败但就算一致服务端也不会拿它当唯一依据因为真正的依据是服务端自己计算出来的完整哈希。3.2 增量式校验的设计大文件合并时如果全量读一遍再算哈希磁盘IO开销很大。一个10GB的文件在合并时还要再完整读一遍做校验速度会明显下降。我采用的增量校验思路是合并线程按分片边界读取数据每读到一片的数据块就直接对比该分片的哈希。分片原本在接收时就已经算过一次哈希合并时按块读取并再次验证这样不需要额外在合并完成后全文件再扫一遍。整文件SHA-256在写入最终文件的同时通过增量哈希对象持续更新。换句话说合并和校验是并行完成的读出来的数据被同时用于“验证分片哈希”和“更新全文件哈希”两个用途磁盘IO上没有额外的二次读取。3.3 哈希算法选型SHA-256是底线不要用MD5。MD5速度快但在高安全要求的场景下它已经不适合作为完整性证明的手段。SHA-256虽然计算开销略高一点但对于服务器而言完全可接受。实测在主流服务器CPU上SHA-256的哈希计算速度普遍能到1GB/s以上相比网络传输的瓶颈根本不算事。还有个小坑不要用文件流直接一次性算哈希要分块读取缓冲。不要用字符串拼接再转byte[]的方式去算容易踩编码坑。直接对Stream按缓冲区读取即可。4. 上传通道的身份认证与链路保护4.1 组件与主系统认证体系的对接上传组件不建议自己维护一套用户和权限体系。军工类项目通常已经有统一身份认证系统组件只需要做对接。我的做法是上传组件接受主系统签发的短期访问令牌令牌中携带用户标识、角色、有效期上传接口从令牌中解析出当前操作人。在ASP.NET Core里用JWT或者自定义签名Token都行重点是要把Token绑定到uploadId。也就是说初始化上传时记录token中的用户身份后续分片请求必须使用同一用户的token否则直接拒绝。4.2 令牌的短期有效性设计大文件上传时间长如果Token有效期只有10分钟传一半就得重新认证体验会很差。但如果Token有效期太长泄露后的风险又太大。我的处理方式是分两层访问Token有效期短15到30分钟用于初始化上传、查询续传元数据。上传会话Token针对uploadId签发有效期覆盖整个上传窗口比如最长48小时但只对这一个小范围的接口有效。上传会话Token在每次续传时都要服务端校验是否和uploadId绑定一旦发现token与uploadId不匹配立刻拒绝并记录异常日志。这样既照顾了长时传输又不会把短Token的暴露面扩大。4.3 防重放、防篡改HTTPS之外还需要什么HTTPS只能保证传输过程中不被监听不能保证请求本身不是攻击者截获后重放的。防重放的手段常用时间戳nonce客户端每个请求带一个时间戳和一个随机nonce服务端检查时间窗口并把已使用的nonce记入缓存重复出现就直接拒绝。防篡改层面可以对分片请求体计算HMAC密钥部署在服务端配置文件里前端由授权逻辑下发密钥。不过要承认完全的前端密钥下发机制很难做到绝对安全所以在高安全场景里更可靠的做法还是以服务端校验为主。每个分片的数据内容安全靠哈希校验兜底请求级别安全靠HTTPSHMAC做加固。这里特别提一下IIS部署场景下经常遇到的一个问题ASP.NET的请求验证机制会拦截包含特殊字符的查询字符串。如果你把文件名、路径信息塞进QueryString有时候会看到“检测到有潜在危险的Request.QueryString值”的报错。我们组件里已经把文件原始名扔进Body或者Header传递不在QueryString里走原始文件名从根上避开这类问题。5. 临时分片隔离、合并原子性与存储区权限控制5.1 临时分片目录的权限模型分片数据在传输过程中是“非完整”的敏感数据但这些临时文件依然需要严格保护。临时目录要满足几个条件不能落在Web站点根目录下防止被静态文件映射直接访问。使用独立账号权限只有运行上传组件服务的账号有读写权限。如果基础环境允许临时目录所在磁盘建议启用磁盘加密防止物理层面被直接读取。定期清理超过一定时间未完成的分片文件避免残留文件堆积。我的经验是在Linux上用file system权限或Windows上用ACL两种方式都要验证“能否从Web路径直接访问到临时文件”。检查方法很简单部署完成后直接用浏览器访问临时目录对应的URL如果返回403或404说明Web层已经挡掉。如果返回了文件名列表那就说明临时目录暴露了。5.2 合并过程的原子性所有分片接收完成后合并操作必须保证原子性。我通常用两步走第一步在同一个临时分区内生成最终的临时文件将所有分片按序合并写入并在合并过程中生成整文件SHA-256。 第二步校验通过后将临时文件通过File.Move或rename方式移动到受控存储目录。因为是在同一个文件系统分区内的rename这个操作是原子的不会出现“移动到一半系统重启导致文件损坏”的情况。这里面有个容易踩的坑合并时如果目标目录和临时目录不在同一分区File.Move会变成“复制删除”复制过程中如果中断目标位置会留下不完整的文件。所以一定确保临时目录和受控存储目录在同一个卷上或者先复制到同卷的临时文件名再rename。5.3 存储目录的分级管理上传完成后的文件要按照数据的敏感程度分目录存储。不要所有文件堆在一个目录里。一个常见的分级思路是一级目录用于普通设计文档、办公文档。二级目录用于核心产品数据、工程数据。三级目录用于需要更高隔离等级的专项数据。每一级目录配置不同的ACL只有有相应权限的服务账号和应用能够访问。存储区不能开放匿名FTP、不能开放共享目录给所有内网用户。如果后续还要做文件分发、预览也应该通过服务端程序间接访问而不是直接暴露存储路径。6. 审计日志与异常检测出了事要能完整回溯6.1 全链路审计日志包含哪些字段军工项目对审计的要求通常比普通项目严格得多。上传组件至少要记录以下内容上传会话初始化uploadId、用户标识、原始文件名、文件大小、客户端IP、时间。每个分片的接收结果uploadId、分片索引、分片大小、分片SHA-256、是否通过校验、耗时。续传查询用户每次查询已接收分片列表都要留痕。合并操作合并开始时间、合并结束时间、最终存储路径、整文件SHA-256。删除操作谁删除了临时目录下的哪个文件、什么时候删除的。日志数据要独立于业务数据库和Web服务器本地日志之外同步到专门的日志服务器或者独立的审计数据库中防止应用被攻破后攻击者顺手把日志也清了。6.2 异常行为的自动标记单纯记录日志还不够我建议在组件里内置几套简单的异常检测规则同一个uploadId在短时间内被多个不同客户端IP请求标记为异常。分片哈希校验连续失败多次标记为异常。单个IP的并发上传会话数超过阈值自动拒绝新增会话。上传频率和正常用户历史行为差异过大标记为可疑。这些规则在真实项目里会有误报但误报好过漏报。标记出来的异常事件自动通知到运维或安全人员由人来判断是否进一步处理。6.3 拿什么保证“日志本身可信”一个容易被忽略的问题是日志被篡改怎么办。常见做法是日志链式哈希每条日志记录除了本身内容还要带上上一条日志的哈希值这样一旦有人在中间改了任何一条日志之后所有记录全部对不上篡改行为立刻暴露。这个思路实现起来并不复杂日志对象里加一个字段存上一条哈希写日志时读最后一条的哈希拼进当前记录再计算新哈希。对上传组件的性能影响可以忽略不计。7. 组件化改造与工程落地的几个经验7.1 组件内部模块划分落地过程中我把组件内部清晰分成了五个模块各有分工模块职责协议层处理初始化、分片上传、续传查询、完成确认的接口校验层哈希计算、文件大小校验、分片合法性校验存储层临时文件读写、分片管理、合并、最终文件归档审计层全链路日志、链式哈希、异常标记配置层集中读取分片大小、目录路径、时间窗口、扩展名白名单模块之间通过接口依赖不直接互相访问内部细节。这样后面如果要把组件接入不同的主系统只需要改协议层或者配置层其他模块不需要动。7.2 配置项没你想的那么多配置项要尽量精简。我最终对外暴露的配置项其实只有这些分片大小、临时目录、最终存储区根目录、允许的扩展名白名单、单文件大小上限、上传会话有效期、Token密钥。很多组件喜欢把大量内部参数暴露成配置实际只会增加出问题的面。特别说明一下允许的扩展名白名单。军工项目里最好不要什么都让传可执行文件、脚本文件、压缩包里的可执行内容都要有严格的检查策略。扩展名白名单只是第一道关卡上传完成后最好再对文件类型做内容级嗅探避免伪造扩展名的情况。7.3 部署在IIS上的几个容易踩的坑如果是在Windows Server的IIS上部署ASP.NET上传组件有几个东西不提前处理很容易上线就出问题第一请求大小限制。IIS的maxAllowedContentLength默认只有约28.6MB需要在web.config里调整system.webServer security requestFiltering requestLimits maxAllowedContentLength104857600 / /requestFiltering /security /system.webServer注意这个单位和ASP.NET的maxRequestLength不一样前者单位是字节后者单位是KB。两个都要检查但我们的组件是分片上传每个分片的大小都很小理论上不会触发这两个限制。不过如果开发过程中直接用Postman测试整个文件上传到接口就会撞上这个坑。第二应用池回收导致临时分片丢失。IIS应用池默认空闲超时是20分钟大文件上传期间如果服务端有超过20分钟没有新分片到达工作进程会被回收内存里的状态全部丢失。我们组件把元数据和分片记录持久化到数据库部分解决了这类问题但为了防止回收过程中出现文件句柄占用异常建议把上传接口相关应用池的“固定时间间隔回收”设为0只保留内存回收阈值同时把空闲回收时间调长。第三HTTPS证书链路问题。内网环境经常自建证书但客户端和服务端之间如果证书链不完整会导致分片上传间歇性失败。排查时先确认协议层用的是TLS1.2及以上。7.4 关于前端Worker上传和“秒传”的补充现在很多前端方案会用Web Worker读取大文件的分片并计算哈希这样避免主线程卡死。这套做法本身是合理的我们组件也支持。但一定要想清楚前端Worker算出来的哈希永远只能作为“辅助参考”不能作为服务端信任的唯一依据服务端必须独立计算分片哈希。之前遇到过一个事故前端Worker在计算分片哈希时因为文件名编码问题算错了和实际上传内容不符客户端一直报校验失败查了很久才发现是前端哈希逻辑的编码问题。服务端独立校验这条底线一直都在最终保证了文件没有被错误覆盖。至于“秒传”也就是根据哈希跳过重复上传这类功能的前提是服务端有一份已经归档文件的哈希索引并且这个索引本身在受控环境内。不要把秒传做成“前端说上传过就跳过”这个我在前面也已经强调过。做这类组件的这几年我最大的体会是安全设计不是最后加几道安全校验接口而是在协议设计之初就把“服务端说了算”这条原则贯彻到底。客户端可以给建议可以给辅助信息但所有影响最终文件内容的决定——偏移量、校验结果、完成条件——都必须由服务端重新计算和验证。断点续传的安全性本质上就是服务端对每一片数据都保持“不轻信”的态度。只要这一条立住了组件后面接入什么样的环境都不会出大方向的问题。