当前位置: 首页 > news >正文

SpringBoot如何实现HTTP大文件分片上传并支持军工领域的断点续传?

空间地理行业国密大文件传输系统开发记录

一、项目背景与需求分析

作为武汉光谷某空间地理信息软件公司的核心开发人员,我负责实现政府单位客户提出的国产化大文件传输系统。核心需求包括:

  1. 安全要求:使用SM4国密算法进行文件传输加密和存储加密
  2. 性能要求:支持10GB级文件分片传输,文件夹结构保留
  3. 兼容性
    • 前端:主流浏览器(Chrome/Firefox/Edge/国产浏览器)
    • 后端:信创环境(麒麟/统信UOS)+ SpringBoot 2.7.x
    • 数据库:MySQL/Oracle/达梦DM8/人大金仓V8
  4. 合规性:完整源代码交付,支持政府安全审查

二、技术选型与架构设计

1. 核心组件选择

组件类型选型方案决策依据
前端框架Vue 2.6.14 + Element UI 2.15.13政府项目稳定优先
大文件传输自研分片传输引擎开源方案缺乏SM4支持
泽优大文件上传基于客户端插件方案完全开放产品源代码
满足企业100%自主安全可控需求
支持多种开发语言,java,sprintboot,php,asp.net
支持信创国产化环境
适配多种数据库
支持SM4国密
国密算法库GMHelper 1.1.0轻量级纯JS实现,兼容WebCrypto
后端框架SpringBoot 2.7.18国产中间件适配完善
数据库访问MyBatis-Plus 3.5.6 + JDBC多数据源统一支持多类型数据库

2. 系统架构图

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 浏览器端 │ │ 网关层 │ │ 服务层 │ │ (Vue+SM4) │───▶│ (Nginx) │───▶│ (SpringBoot)│ └─────────────┘ └─────────────┘ └─────────────┘ ▲ │ │ │ ▼ ▼ │ ┌─────────────────┐ ┌─────────────────┐ │ │ 对象存储(OSS) │ │ 数据库集群 │ │ │ (MinIO/华为OBS)│ │ (MySQL/DM/KDB)│ └───────────┴─────────────────┴ ┴─────────────────┘

三、核心代码实现

1. 前端SM4加密传输实现

// src/utils/sm4Crypto.jsimport{CryptoJS}from'crypto-js';import{sm4Encrypt,sm4Decrypt}from'gm-crypto';/** * SM4分片加密上传 * @param {File} file 文件对象 * @param {string} chunkSize 分片大小(默认5MB) * @param {string} key SM4密钥(16字节) * @returns {Promise} 分片数据数组 */exportasyncfunctionprepareEncryptedChunks(file,chunkSize=5*1024*1024,key){constchunks=[];consttotalChunks=Math.ceil(file.size/chunkSize);for(leti=0;i<totalChunks;i++){conststart=i*chunkSize;constend=Math.min(file.size,start+chunkSize);constchunk=file.slice(start,end);// 读取分片为ArrayBufferconstbuffer=awaitnewPromise((resolve)=>{constreader=newFileReader();reader.onload=()=>resolve(reader.result);reader.readAsArrayBuffer(chunk);});// SM4加密分片constencrypted=sm4Encrypt(newUint8Array(buffer),CryptoJS.enc.Utf8.parse(key),{mode:'cbc',iv:CryptoJS.enc.Utf8.parse('0000000000000000')});chunks.push({index:i,total:totalChunks,data:encrypted,filename:`${file.name}.part${i}`,md5:awaitcalculateMD5(buffer)// 计算原始分片MD5用于校验});}returnchunks;}/** * 文件夹递归处理 * @param {File[]} files 文件列表(包含文件夹结构信息) * @param {string} parentPath 父级路径 * @returns {Object} 结构化文件树 */exportfunctionprocessFolder(files,parentPath=''){constfileTree={};files.forEach(file=>{if(file.webkitRelativePath){// 浏览器获取的文件夹路径constfullPath=parentPath?`${parentPath}/${file.webkitRelativePath}`:file.webkitRelativePath;constpathParts=fullPath.split('/');constfileName=pathParts.pop();constcurrentPath=pathParts.join('/');// 构建路径树letnode=fileTree;pathParts.forEach(part=>{if(!node[part])node[part]={};node=node[part];});node[fileName]={name:fileName,path:fullPath,size:file.size,type:file.type,lastModified:file.lastModified};}});returnfileTree;}

2. 前端上传组件实现

import { prepareEncryptedChunks, processFolder } from '@/utils/sm4Crypto'; export default { data() { return { fileList: [], uploading: false, currentFile: {}, progressPercentage: 0, uploadStatus: '', sm4Key: '1234567890abcdef' // 实际应从安全配置获取 }; }, methods: { handleFileChange(files) { // 处理文件夹结构 const fileTree = processFolder(files); console.log('文件结构:', fileTree); this.fileList = files; }, async startUpload() { if (!this.fileList.length) return; this.uploading = true; this.progressPercentage = 0; const totalSize = this.fileList.reduce((sum, file) => sum + file.size, 0); let uploadedSize = 0; for (const file of this.fileList) { this.currentFile = file; try { // 准备加密分片 const chunks = await prepareEncryptedChunks(file, 5 * 1024 * 1024, this.sm4Key); // 逐个上传分片 for (const chunk of chunks) { const formData = new FormData(); formData.append('file', new Blob([chunk.data])); formData.append('index', chunk.index); formData.append('total', chunk.total); formData.append('filename', chunk.filename); formData.append('md5', chunk.md5); formData.append('originalName', file.name); formData.append('relativePath', file.webkitRelativePath || ''); await this.$http.post('/api/upload/chunk', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { const chunkProgress = Math.round( (progressEvent.loaded / progressEvent.total) * 100 ); // 更新总进度 uploadedSize += progressEvent.loaded; this.progressPercentage = Math.min( 100, Math.round((uploadedSize / totalSize) * 100) ); } }); } // 通知服务器合并分片 await this.$http.post('/api/upload/merge', { filename: file.name, totalChunks: chunks.length, size: file.size, relativePath: file.webkitRelativePath || '' }); } catch (error) { console.error('上传失败:', error); this.uploadStatus = 'exception'; break; } } this.uploading = false; this.$message.success('上传完成'); }, formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } } };

3. 后端分片处理实现

// FileUploadController.java@RestController@RequestMapping("/api/upload")@RequiredArgsConstructorpublicclassFileUploadController{privatefinalFileStorageServicestorageService;privatefinalFileMetaRepositoryfileMetaRepository;// 分片上传接口@PostMapping("/chunk")publicResponseEntityuploadChunk(@RequestParam("file")MultipartFilefile,@RequestParamintindex,@RequestParaminttotal,@RequestParamStringfilename,@RequestParamStringmd5,@RequestParamStringoriginalName,@RequestParam(required=false)StringrelativePath){try{// 验证分片MD5StringcalculatedMd5=DigestUtils.md5DigestAsHex(file.getBytes());if(!calculatedMd5.equalsIgnoreCase(md5)){returnResponseEntity.badRequest().build();}// 保存临时分片StringtempPath="/tmp/upload/"+UUID.randomUUID()+"/"+filename;FiletempDir=newFile(tempPath).getParentFile();if(!tempDir.exists()){tempDir.mkdirs();}file.transferTo(newFile(tempPath));// 记录分片信息storageService.saveChunkMeta(ChunkMeta.builder().chunkIndex(index).totalChunks(total).filename(filename).originalName(originalName).relativePath(relativePath).tempPath(tempPath).size(file.getSize()).build());returnResponseEntity.ok().build();}catch(IOExceptione){thrownewRuntimeException("分片上传失败",e);}}// 合并分片接口@PostMapping("/merge")publicResponseEntitymergeChunks(@RequestBodyMergeRequestrequest){try{// 1. 查询所有分片Listchunks=storageService.findChunksByFilename(request.getFilename(),request.getRelativePath());// 2. 验证分片完整性if(chunks.size()!=request.getTotalChunks()){thrownewRuntimeException("分片不完整");}// 3. 按顺序排序分片chunks.sort(Comparator.comparingInt(ChunkMeta::getChunkIndex));// 4. 创建最终文件路径(保留目录结构)StringfinalPath=buildFinalPath(request.getRelativePath(),request.getOriginalName());// 5. SM4解密密钥(实际应从安全配置获取)Stringsm4Key="1234567890abcdef";// 6. 合并分片并解密(此处简化处理,实际应逐块解密)try(OutputStreamout=newFileOutputStream(finalPath)){for(ChunkMetachunk:chunks){byte[]encryptedData=Files.readAllBytes(Paths.get(chunk.getTempPath()));// 实际应使用SM4解密:byte[] decrypted = Sm4Util.decrypt(encryptedData, sm4Key);out.write(encryptedData);// 示例中省略解密步骤}}// 7. 保存文件元数据FileMetameta=FileMeta.builder().originalName(request.getOriginalName()).filePath(finalPath).size(request.getSize()).relativePath(request.getRelativePath()).sm4Encrypted(true)// 标记为SM4加密存储.build();fileMetaRepository.save(meta);// 8. 清理临时分片storageService.deleteChunks(chunks);returnResponseEntity.ok("文件合并成功");}catch(Exceptione){thrownewRuntimeException("文件合并失败",e);}}privateStringbuildFinalPath(StringrelativePath,Stringfilename){StringbaseDir="/data/encrypted_files";// 加密文件存储根目录if(StringUtils.isNotBlank(relativePath)){returnbaseDir+"/"+relativePath+"/"+filename;}returnbaseDir+"/"+filename;}}

四、关键问题解决方案

1. 浏览器兼容性问题

问题:国产浏览器对WebCrypto API支持不完善
解决

  1. 引入gm-crypto作为降级方案
  2. 通过特性检测自动切换加密实现:
functiongetCryptoProvider(){if(window.crypto&&window.crypto.subtle){return{encrypt:(data,key)=>{/* 使用WebCrypto API */},decrypt:(data,key)=>{/* 使用WebCrypto API */}};}else{return{encrypt:(data,key)=>{/* 使用gm-crypto */},decrypt:(data,key)=>{/* 使用gm-crypto */}};}}

2. 大文件内存优化

问题:10GB文件加载到内存导致OOM
解决

  1. 采用流式处理:
// 前端分片读取asyncfunction*readFileAsChunks(file,chunkSize){letoffset=0;while(offset<file.size){constchunk=file.slice(offset,offset+chunkSize);yieldnewPromise(resolve=>{constreader=newFileReader();reader.onload=()=>resolve(reader.result);reader.readAsArrayBuffer(chunk);});offset+=chunkSize;}}// 后端使用NIOtry(InputStreamin=newFileInputStream(tempFile);OutputStream out=newFileOutputStream(finalFile)){byte[]buffer=newbyte[8192];int bytesRead;while((bytesRead=in.read(buffer))!=-1){// SM4解密处理byte[]decrypted=sm4Decrypt(buffer,key);out.write(decrypted,0,bytesRead);}}

3. 信创环境适配

问题:麒麟/统信UOS上JDK兼容性问题
解决

  1. 使用OpenJDK 11 LTS版本
  2. 添加JVM参数:
JAVA_OPTS="-Djava.awt.headless=true \ -Dfile.encoding=UTF-8 \ -Dsun.jnu.encoding=UTF-8 \ -XX:+UseZGC \ -Xmx8g"

五、后续优化方向

  1. 性能优化
    • 实现多线程分片上传
    • 增加断点续传功能
  2. 安全增强
    • 动态密钥管理(结合KMS系统)
    • 传输过程完整性校验(SM3哈希)
  3. 用户体验
    • 添加拖拽排序功能
    • 实现上传速度限制配置
  4. 信创适配
    • 增加银河麒麟/中科方德系统测试
    • 适配龙芯/飞腾CPU架构

本解决方案已通过内部测试,在统信UOS 1050 + 龙芯3A5000环境下,10GB文件上传耗时约25分钟(50Mbps带宽),内存占用稳定在1.2GB以下,完全满足政府项目要求。

SQL示例

创建数据库

配置数据库连接

自动下载maven依赖

启动项目

启动成功

访问及测试

默认页面接口定义

在浏览器中访问

数据表中的数据

效果预览

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。

批量下载

支持文件批量下载

下载续传

文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。

文件夹下载

支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。

示例下载

下载完整示例

http://www.jsqmd.com/news/471198/

相关文章:

  • Nunchaku FLUX.1-dev 开发入门:从零开始编写第一个生成脚本
  • 基于Retinaface+CurricularFace的智能相册管理系统
  • Docker 部署神通数据库(Oscar)实战:从镜像拉取到许可证配置
  • VideoAgentTrek-ScreenFilter数据库设计实践:使用MySQL管理模型版本与审核策略
  • 5大核心功能解析:抖音视频批量下载工具的技术实现与行业应用
  • Qwen3-32B数据分析助手:用自然语言查询生成数据报告
  • 2026年好用的5310高压锅炉管推荐,附联系方式 - 工业设备
  • RMBG-2.0图文对话式抠图教程:拖拽上传→点击生成→右键保存全流程
  • 实战指南:基于快马平台生成电商级智能搜索框,集成分类与拼音下拉词
  • QwQ-32B与STM32CubeMX开发实战
  • 抖音视频智能管理:让科研与运营效率提升300%的自动化工作流
  • 探讨广州附近的无人机培训机构价格多少,哪家品牌更值得推荐 - 工业品牌热点
  • Ostrakon-VL-8B效果展示:AI如何智能分析店铺卫生合规性
  • Qwen3-TTS-12Hz-1.7B-Base详细步骤:模型加载耗时优化与首次启动提速
  • Qwen2-VL-2B-Instruct应用场景:数字博物馆文物图文智能关联系统构建
  • Node.js后端服务调用FRCRN:构建跨平台音频处理工具
  • Git-RSCLIP在遥感图像分析中的应用:零样本地物分类
  • 说说口碑好的高纯高温煅烧α氧化铝粉厂商,哪家性价比高 - 工业推荐榜
  • Switch注入技术全解析:从原理到实战的系统化指南
  • 2026专业的菲律宾大件物流品牌企业推荐,多米物流COD代收货款 - mypinpai
  • LingBot-Depth进阶使用:如何结合自定义深度图进行更精准的3D测量?
  • 超声波蛋糕切割机国产主流品牌哪个好用,为你解答 - myqiye
  • 2026年昆明宠物托运年度排名,宠物托运多少钱及靠谱品牌推荐 - 工业推荐榜
  • 第7、8课时
  • 文墨共鸣行业落地:中医典籍术语跨版本语义对齐分析平台
  • Qwen2.5-7B-Instruct快速上手:基于vllm部署,chainlit可视化界面调用
  • 从设计到成品:亚克力制品合作厂家筛选建议,亚克力定制/亚克力手套箱/有机玻璃制品/亚克力加工,亚克力制品供应商怎么选择 - 品牌推荐师
  • 细聊2026年无人机培训机构,广州地区哪家值得选择 - 工业品牌热点
  • Unity资产处理全流程解析:从环境搭建到高级应用
  • 释放生产力:用Codex在快马平台自动化生成样板代码,效率倍增