大文件传输系统设计方案(基于SM4国密算法)
需求分析
作为四川某软件公司的开发人员,我面临以下核心需求:
- 实现10GB级别大文件的分片上传/下载
- 采用国密SM4算法进行端到端加密
- 服务端需支持SM4加密存储
- 兼容主流浏览器及信创国产化环境
- 基于SpringBoot+Vue技术栈
- 需要开源可审查的代码
技术选型
经过调研,我决定采用以下技术方案:
前端方案
- 基于Vue-cli构建
- 使用自定义分片上传组件(替代已停更的WebUploader)
- 集成SM4加密的JavaScript实现(通过wasm或纯JS实现)
后端方案
- SpringBoot 2.7.x
- 集成BouncyCastle的SM4实现
- 支持分片合并和加密存储
前端核心代码实现
1. SM4加密工具类 (sm4-utils.js)
// 使用wasm版本的SM4实现(性能更好)
let sm4Module = null;
export async function initSM4() {
if (sm4Module) return;
try {
sm4Module = await import('sm4-wasm');
await sm4Module.default(); // 初始化wasm模块
} catch (e) {
console.error('SM4 WASM加载失败,降级使用JS实现', e);
// 降级方案:使用纯JS实现
await import('./sm4-js').then(module => {
sm4Module = module;
});
}
}
export function encryptFileChunk(chunk, key, iv) {
if (!sm4Module) throw new Error('SM4未初始化');
// 转换为ArrayBuffer
const arrayBuffer = chunk instanceof ArrayBuffer
? chunk
: await new Response(chunk).arrayBuffer();
// 使用SM4-CBC模式加密
return sm4Module.encrypt(arrayBuffer, key, iv, 'CBC');
}
export function decryptFileChunk(encryptedChunk, key, iv) {
if (!sm4Module) throw new Error('SM4未初始化');
return sm4Module.decrypt(encryptedChunk, key, iv, 'CBC');
}
2. 分片上传组件 (FileUploader.vue)
import { initSM4, encryptFileChunk } from './sm4-utils';
// 生成随机IV
function generateIV() {
return crypto.getRandomValues(new Uint8Array(16));
}
export default {
data() {
return {
file: null,
uploading: false,
progress: 0,
chunkSize: 5 * 1024 * 1024, // 5MB每片
sm4Key: null, // 实际应从安全渠道获取
fileId: null
};
},
async mounted() {
await initSM4();
// 生成随机密钥(实际项目应从安全渠道获取)
this.sm4Key = crypto.getRandomValues(new Uint8Array(16));
},
methods: {
handleFileChange(e) {
this.file = e.target.files[0];
this.progress = 0;
},
async startUpload() {
if (!this.file) return;
this.uploading = true;
this.progress = 0;
try {
// 1. 初始化上传,获取fileId
const initRes = await this.$http.post('/api/upload/init', {
fileName: this.file.name,
fileSize: this.file.size,
chunkSize: this.chunkSize
});
this.fileId = initRes.data.fileId;
const iv = generateIV();
// 2. 上传加密分片
const chunkCount = Math.ceil(this.file.size / this.chunkSize);
let uploadedChunks = 0;
for (let i = 0; i < chunkCount; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
// 加密分片
const encryptedChunk = await encryptFileChunk(chunk, this.sm4Key, iv);
// 上传分片
const formData = new FormData();
formData.append('fileId', this.fileId);
formData.append('chunkIndex', i);
formData.append('chunk', new Blob([encryptedChunk]));
formData.append('iv', new Blob([iv])); // 每个分片使用相同IV(简单实现)
await this.$http.post('/api/upload/chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
uploadedChunks++;
this.progress = Math.floor((uploadedChunks / chunkCount) * 100);
}
// 3. 完成上传
await this.$http.post('/api/upload/complete', {
fileId: this.fileId,
iv: Array.from(iv).join(',') // 实际应使用更安全的方式传输IV
});
this.$emit('upload-success', { fileId: this.fileId, fileName: this.file.name });
} catch (error) {
console.error('上传失败:', error);
this.$emit('upload-error', error);
} finally {
this.uploading = false;
}
}
}
};
后端核心代码实现
1. SM4工具类 (SM4Util.java)
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
import java.util.Base64;
public class SM4Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 加密文件分片
public static byte[] encrypt(byte[] key, byte[] iv, byte[] input) throws Exception {
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
new CBCBlockCipher(new SM4Engine()));
cipher.init(true, new ParametersWithIV(new KeyParameter(key), iv));
byte[] output = new byte[cipher.getOutputSize(input.length)];
int len = cipher.processBytes(input, 0, input.length, output, 0);
len += cipher.doFinal(output, len);
if (len < output.length) {
byte[] result = new byte[len];
System.arraycopy(output, 0, result, 0, len);
return result;
}
return output;
}
// 解密文件分片
public static byte[] decrypt(byte[] key, byte[] iv, byte[] input) throws Exception {
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
new CBCBlockCipher(new SM4Engine()));
cipher.init(false, new ParametersWithIV(new KeyParameter(key), iv));
byte[] output = new byte[cipher.getOutputSize(input.length)];
int len = cipher.processBytes(input, 0, input.length, output, 0);
len += cipher.doFinal(output, len);
if (len < output.length) {
byte[] result = new byte[len];
System.arraycopy(output, 0, result, 0, len);
return result;
}
return output;
}
// 使用JCE方式(备选方案)
public static byte[] encryptJCE(byte[] key, byte[] iv, byte[] input) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new javax.crypto.spec.IvParameterSpec(iv));
return cipher.doFinal(input);
}
}
2. 文件上传控制器 (FileUploadController.java)
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
@Value("${file.upload-dir}")
private String uploadDir;
// 临时存储上传状态
private final ConcurrentHashMap sessions = new ConcurrentHashMap<>();
// 初始化上传
@PostMapping("/init")
public UploadInitResponse initUpload(@RequestBody UploadInitRequest request) {
String fileId = UUID.randomUUID().toString();
UploadSession session = new UploadSession();
session.setFileName(request.getFileName());
session.setTotalSize(request.getFileSize());
session.setChunkSize(request.getChunkSize());
session.setReceivedChunks(0);
sessions.put(fileId, session);
// 创建临时目录
new File(uploadDir + "/" + fileId).mkdirs();
return new UploadInitResponse(fileId);
}
// 上传分片
@PostMapping("/chunk")
public void uploadChunk(
@RequestParam("fileId") String fileId,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("chunk") MultipartFile chunkFile,
@RequestParam("iv") String ivBase64) throws Exception {
UploadSession session = sessions.get(fileId);
if (session == null) {
throw new RuntimeException("无效的上传会话");
}
// 解密分片(实际项目中密钥应从安全渠道获取)
byte[] key = hexStringToByteArray("你的SM4密钥16字节"); // 示例,实际应从配置或安全存储获取
byte[] iv = Base64.getDecoder().decode(ivBase64);
byte[] encryptedChunk = chunkFile.getBytes();
byte[] decryptedChunk = SM4Util.decrypt(key, iv, encryptedChunk);
// 保存分片
String chunkPath = uploadDir + "/" + fileId + "/" + chunkIndex;
try (FileOutputStream fos = new FileOutputStream(chunkPath)) {
fos.write(decryptedChunk);
}
session.setReceivedChunks(session.getReceivedChunks() + 1);
}
// 完成上传
@PostMapping("/complete")
public UploadCompleteResponse completeUpload(@RequestBody UploadCompleteRequest request) throws Exception {
String fileId = request.getFileId();
UploadSession session = sessions.get(fileId);
if (session == null) {
throw new RuntimeException("无效的上传会话");
}
// 合并分片
String tempDir = uploadDir + "/" + fileId;
String finalPath = uploadDir + "/" + fileId + ".enc"; // 加密存储
try (FileOutputStream fos = new FileOutputStream(finalPath)) {
for (int i = 0; i < session.getTotalChunks(); i++) {
byte[] chunk = Files.readAllBytes(Paths.get(tempDir + "/" + i));
fos.write(chunk);
}
}
// 清理临时文件
Files.walk(Paths.get(tempDir))
.sorted((a, b) -> b.compareTo(a)) // 反向排序先删文件再删目录
.forEach(path -> {
try {
Files.delete(path);
} catch (Exception e) {
// 记录日志
}
});
// 存储文件元信息(实际项目应存入数据库)
FileMeta meta = new FileMeta();
meta.setFileId(fileId);
meta.setOriginalName(session.getFileName());
meta.setEncryptedPath(finalPath);
meta.setSize(session.getTotalSize());
// 这里应该将meta保存到数据库
return new UploadCompleteResponse(fileId, session.getFileName());
}
// 计算总分片数
private int calculateTotalChunks(long fileSize, int chunkSize) {
return (int) Math.ceil((double) fileSize / chunkSize);
}
// 内部类定义省略...
}
信创环境兼容性考虑
-
前端兼容性:
- 提供wasm和纯JS两套SM4实现,wasm优先
- 使用标准的Web API,避免使用实验性功能
- 测试环境包括:麒麟OS+飞腾CPU、UOS+龙芯等组合
-
后端兼容性:
- 使用BouncyCastle作为密码学提供者,兼容国产CPU指令集
- 避免使用与特定JDK版本绑定的API
- 测试环境包括:中科方德JDK、华为Kunpeng JDK等
安全考虑
-
密钥管理:
- 示例中简化了密钥处理,实际项目应:
- 使用硬件加密机或软HSM管理主密钥
- 每个文件使用随机生成的密钥,通过主密钥加密后传输
- 考虑使用国密SSL/TLS (GMTLS)
-
传输安全:
- 所有通信必须通过HTTPS
- 考虑使用双向SSL认证
-
存储安全:
- 服务端存储的是加密后的文件
- 只有授权用户才能获取解密密钥
性能优化建议
- 前端可实现断点续传,记录已上传分片
- 后端可使用异步处理合并分片操作
- 对于超大文件,考虑使用更高效的存储格式
- 实现分片校验机制,确保数据完整性
总结
本方案提供了完整的SM4加密大文件传输实现,从前端分片加密到后端存储都符合国密要求。代码结构清晰,便于进行源代码审查。虽然实现起来比使用现成组件复杂,但完全可控且符合政府单位的安全要求。
下一步工作建议:
- 完善密钥管理方案
- 增加下载功能实现
- 添加更完善的错误处理和日志
- 进行全面的性能测试和安全审计
将组件复制到项目中
示例中已经包含此目录
引入组件
配置接口地址
接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
处理事件
启动测试
启动成功
效果
数据库
效果预览
文件上传
文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
批量下载
支持文件批量下载
下载续传
文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。
文件夹下载
支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。