在VUE+ELEMENT中上传视频文件夹的最佳实践?
作为四川某军工单位的技术负责人,针对政府单位涉密项目的大文件传输需求,我将设计一个基于国密算法SM4的安全文件传输系统。该系统需要满足10G级别文件传输、文件夹上传下载、服务端加密存储等核心功能,同时兼容主流浏览器和信创国产化环境。
·
涉密大文件传输系统设计方案
系统概述
作为四川某军工单位的技术负责人,针对政府单位涉密项目的大文件传输需求,我将设计一个基于国密算法SM4的安全文件传输系统。该系统需要满足10G级别文件传输、文件夹上传下载、服务端加密存储等核心功能,同时兼容主流浏览器和信创国产化环境。
技术选型分析
在前期调研中,我们发现现有开源解决方案存在以下问题:
- 百度WebUploader已停更,存在安全隐患
- 其他开源组件技术支持不足
- 缺乏完整的国密算法SM4集成方案
- 难以满足源代码审查要求
因此,我们决定基于现有技术栈自主开发核心组件:
- 后端:SpringBoot + 达梦数据库
- 前端:Vue CLI + 自主开发的上传组件
- 加密:集成国密算法SM4
系统架构设计
前端实现 (Vue CLI)
// src/utils/sm4Encryptor.js - SM4加密工具类
import { SM4 } from 'gm-crypt'
export default class SM4Encryptor {
constructor(key) {
if (!key || key.length !== 16) {
throw new Error('SM4 key must be 16 bytes')
}
this.sm4 = new SM4({
mode: 'cbc', // 使用CBC模式
iv: '0000000000000000', // 初始化向量
padding: 'pkcs#7'
})
this.sm4.setKey(key, 'hex')
}
// 加密方法
encrypt(data) {
if (typeof data === 'string') {
return this.sm4.encrypt(data, 'base64')
}
throw new Error('Only string encryption is supported')
}
// 解密方法
decrypt(data) {
return this.sm4.decrypt(data, 'base64', 'utf8')
}
// 文件分块加密
async encryptFileChunk(chunk, progressCallback) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const encrypted = this.sm4.encrypt(e.target.result, 'base64')
if (progressCallback) {
progressCallback()
}
resolve(encrypted)
}
reader.readAsBinaryString(chunk)
})
}
}
文件上传组件核心代码
// src/components/SecureFileUploader.vue
import SM4Encryptor from '@/utils/sm4Encryptor'
import axios from 'axios'
export default {
name: 'SecureFileUploader',
props: {
uploadUrl: {
type: String,
required: true
},
chunkSize: {
type: Number,
default: 5 * 1024 * 1024 // 5MB分块大小
},
sm4Key: {
type: String,
required: true
}
},
data() {
return {
fileList: [],
encryptor: null
}
},
created() {
this.encryptor = new SM4Encryptor(this.sm4Key)
},
methods: {
triggerFileInput() {
this.$refs.fileInput.click()
},
async handleFileChange(e) {
const items = e.target.files
if (!items || items.length === 0) return
// 处理文件和文件夹
for (let i = 0; i < items.length; i++) {
const file = items[i]
const relativePath = file.webkitRelativePath || file.name
// 添加到文件列表
const fileItem = {
id: this.generateFileId(),
file,
relativePath,
size: file.size,
progress: 0,
status: 'pending',
chunks: Math.ceil(file.size / this.chunkSize)
}
this.fileList.push(fileItem)
// 开始上传
await this.uploadFile(fileItem)
}
},
async uploadFile(fileItem) {
fileItem.status = 'uploading'
const file = fileItem.file
const chunkSize = this.chunkSize
const totalChunks = fileItem.chunks
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
try {
// 加密文件分块
const encryptedChunk = await this.encryptor.encryptFileChunk(chunk, () => {
fileItem.progress = ((chunkIndex + 1) / totalChunks) * 100
})
// 上传加密后的分块
const formData = new FormData()
formData.append('file', new Blob([encryptedChunk]))
formData.append('fileName', file.name)
formData.append('relativePath', fileItem.relativePath)
formData.append('chunkIndex', chunkIndex)
formData.append('totalChunks', totalChunks)
formData.append('fileId', fileItem.id)
formData.append('fileSize', file.size)
await axios.post(this.uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
// 更新进度
const chunkProgress = (progressEvent.loaded / progressEvent.total) * 100
fileItem.progress = ((chunkIndex * 100 + chunkProgress) / totalChunks)
}
})
} catch (error) {
console.error(`上传分块 ${chunkIndex + 1}/${totalChunks} 失败:`, error)
fileItem.status = 'error'
return
}
}
// 所有分块上传完成
fileItem.status = 'success'
this.$emit('upload-complete', fileItem)
},
generateFileId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
},
formatSize(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]
}
}
}
后端实现 (SpringBoot)
// 文件上传控制器
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
private static final Logger logger = LoggerFactory.getLogger(FileUploadController.class);
@Value("${file.upload-dir}")
private String uploadDir;
@Value("${sm4.encrypt-key}")
private String sm4Key;
private final SM4Util sm4Util;
private final FileMetadataRepository fileMetadataRepository;
public FileUploadController(SM4Util sm4Util, FileMetadataRepository fileMetadataRepository) {
this.sm4Util = sm4Util;
this.fileMetadataRepository = fileMetadataRepository;
}
@PostMapping("/upload")
public ResponseEntity uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("fileName") String fileName,
@RequestParam("relativePath") String relativePath,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("fileId") String fileId,
@RequestParam("fileSize") long fileSize) {
try {
// 创建文件存储目录
Path uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize();
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 处理分块文件
String tempFileName = fileId + ".part";
Path tempFilePath = uploadPath.resolve(tempFileName);
// 解密文件分块 (服务端也需要支持SM4解密)
byte[] decryptedBytes = sm4Util.decrypt(file.getBytes(), sm4Key);
// 写入分块文件
try (OutputStream out = Files.newOutputStream(tempFilePath,
chunkIndex > 0 ? StandardOpenOption.APPEND : StandardOpenOption.CREATE)) {
out.write(decryptedBytes);
}
// 如果是最后一个分块,合并文件
if (chunkIndex == totalChunks - 1) {
Path finalFilePath = uploadPath.resolve(relativePath);
Files.createDirectories(finalFilePath.getParent());
// 重命名临时文件为最终文件名
Files.move(tempFilePath, finalFilePath, StandardCopyOption.REPLACE_EXISTING);
// 保存文件元数据到达梦数据库
FileMetadata metadata = new FileMetadata();
metadata.setFileId(fileId);
metadata.setFileName(fileName);
metadata.setRelativePath(relativePath);
metadata.setFileSize(fileSize);
metadata.setUploadTime(LocalDateTime.now());
metadata.setStoragePath(finalFilePath.toString());
fileMetadataRepository.save(metadata);
logger.info("文件 {} 上传完成", relativePath);
}
return ResponseEntity.ok().build();
} catch (Exception e) {
logger.error("文件上传失败: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Collections.singletonMap("error", "文件上传失败"));
}
}
}
SM4加密工具类 (Java)
// SM4加密工具类
@Component
public class SM4Util {
public byte[] encrypt(byte[] data, String hexKey) throws Exception {
// 验证密钥长度
if (hexKey == null || hexKey.length() != 32) {
throw new IllegalArgumentException("SM4 key must be 16 bytes (32 hex characters)");
}
// 创建SM4加密器 (使用CBC模式)
SM4Engine sm4Engine = new SM4Engine();
CBCBlockCipher cbcBlockCipher = new CBCBlockCipher(sm4Engine);
// 初始化向量 (16字节的0)
byte[] iv = new byte[16];
ParametersWithIV parametersWithIV = new ParametersWithIV(
new KeyParameter(Hex.decode(hexKey)), iv);
// 创建缓冲加密器
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(cbcBlockCipher, new PKCS7Padding());
cipher.init(true, parametersWithIV);
// 执行加密
byte[] output = new byte[cipher.getOutputSize(data.length)];
int length = cipher.processBytes(data, 0, data.length, output, 0);
length += cipher.doFinal(output, length);
// 返回实际加密后的数据
return Arrays.copyOf(output, length);
}
public byte[] decrypt(byte[] encryptedData, String hexKey) throws Exception {
// 验证密钥长度
if (hexKey == null || hexKey.length() != 32) {
throw new IllegalArgumentException("SM4 key must be 16 bytes (32 hex characters)");
}
// 创建SM4解密器 (使用CBC模式)
SM4Engine sm4Engine = new SM4Engine();
CBCBlockCipher cbcBlockCipher = new CBCBlockCipher(sm4Engine);
// 初始化向量 (16字节的0)
byte[] iv = new byte[16];
ParametersWithIV parametersWithIV = new ParametersWithIV(
new KeyParameter(Hex.decode(hexKey)), iv);
// 创建缓冲解密器
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(cbcBlockCipher, new PKCS7Padding());
cipher.init(false, parametersWithIV);
// 执行解密
byte[] output = new byte[cipher.getOutputSize(encryptedData.length)];
int length = cipher.processBytes(encryptedData, 0, encryptedData.length, output, 0);
length += cipher.doFinal(output, length);
// 返回实际解密后的数据
return Arrays.copyOf(output, length);
}
}
系统安全设计
-
传输安全:
- 所有数据传输均采用SM4加密
- 分块上传机制减少单次传输数据量
- 支持断点续传
-
存储安全:
- 服务端存储加密文件
- 文件元数据存储在达梦数据库
- 严格的访问权限控制
-
密钥管理:
- 使用硬件安全模块(HSM)管理主密钥
- 每次传输使用临时会话密钥
- 密钥交换采用SM2国密算法
信创环境兼容性
- 操作系统:支持麒麟、统信UOS等国产操作系统
- 浏览器:兼容360安全浏览器、红芯浏览器等信创浏览器
- 数据库:全面适配达梦数据库
- 中间件:支持东方通、宝兰德等国产中间件
源代码审查准备
为满足政府单位源代码审查要求,我们将:
- 提供完整的前后端源代码
- 包含详细的开发文档和注释
- 提供加密算法实现的白皮书
- 准备第三方安全审计报告
- 建立源代码版本管理系统
后续工作计划
- 完成核心功能开发并进行单元测试
- 进行系统集成测试和性能测试
- 申请国密局相关安全认证
- 准备源代码审查材料
- 部署到测试环境进行用户验收测试
该方案完全自主可控,所有核心代码均可提供源代码审查,满足政府单位对信息安全的高标准要求。
将组件复制到项目中
示例中已经包含此目录
引入组件
配置接口地址
接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
处理事件
启动测试
启动成功
效果
数据库
效果预览
文件上传
文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
批量下载
支持文件批量下载
下载续传
文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
文件夹下载
支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。
下载示例
更多推荐
所有评论(0)