本篇文章包括以下内容:
- MinIO加密实现方案分析
- 使用代码实现加密的工具类
MinIO的加密实现涵盖了多个层面,包括数据传输和数据存储。
1.使用HTTPS
HTTPS是一种安全的通信协议,它在传输层通过SSL/TLS协议对数据进行加密。MinIO支持HTTPS,确保数据在传输过程中的安全。用户可以通过配置MinIO服务器的SSL证书来启用HTTPS。
2.使用TLS
传输层安全协议(TLS)是一种用于在网络通信中提供隐私和数据完整性的协议。MinIO支持TLS,用户可以通过配置TLS证书来增强数据传输的安全性。
3.使用KMS(密钥管理服务)
KMS是一种用于管理加密密钥的服务。MinIO可以通过集成KMS来实现更高级的加密管理。用户可以将密钥存储在KMS中,并使用这些密钥对数据进行加密和解密。
数据加密的配置和管理
在MinIO中配置加密选项:1.配置HTTPS:在MinIO服务器上配置SSL证书,启用HTTPS。2.配置TLS:在MinIO服务器上配置TLS证书,确保数据传输的安全。3.配置KMS:集成KMS服务,管理加密密钥,并在MinIO中使用这些密钥进行数据加密。MinIO SSE-KMS 使用由密钥管理系统(Key Management System,KMS)管理的 外部密钥(External Key,EK)来加密/解密对象。每个存储桶和对象可以有一个单独的外部密钥(EK),这支持在部署中执行更细粒度的加密操作。 MinIO只能解密一个对象, 如果它可以访问到加密该对象的KMS(密钥管理系统) 和EK(外部密钥)。
在 MinIO 中,最简单的加密方式是使用服务器端加密(SSE-S3)。这种方式不需要客户端进行任何加密操作,所有的加密和解密都是由 MinIO 服务器自动处理的。
关于KMS服务的费用,取决于选择的提供商:
- 付费服务:像AWS KMS、Google Cloud KMS和Azure Key Vault等云服务提供商通常提供按使用量付费的KMS服务。
- 免费服务:有些KMS解决方案,如开源的HashiCorp Vault,可以免费使用,但可能需要自行管理和维护。
MinIO SSE使用MinIO密钥加密服务(KES)和一个受支持的外部密钥管理服务(KMS)用于大规模执行安全加密操作。MinIO还支持客户端管理的密钥管理,其中应用程序完全负责创建和管理用于MinIO SSE的加密密钥。
也就是说,利用MinIO的KMS服务实现加密,需要额外使用的第三方服务为mc、KES、KMS。
- KMS 是一个抽象的密钥管理服务,可以由不同的提供商实现,如Vault或其他云服务。
- KES 是Minio的一个组件,它使用KMS提供的密钥管理功能来为Minio服务器提供加密服务。
- mc 是一个客户端工具,可以与Minio服务器、KMS和KES交互,以执行加密和解密操作。
为了减少对第三方服务的依赖,且便于维护,或方便后期可能更换加密算法为国密算法,本文直接通过编写代码实现文件的加密存储。该实现过程相对简单,涉及以下步骤:在文件上传时,首先捕获文件流,然后应用适当的加密算法对其进行加密,最后将加密后的文件直接存储到目标服务器。由于加解密过程都在服务端,因此我们选择了简便的对称加密算法来完成这一任务。
对称加密算法:
- 密钥特点:使用相同的密钥进行加密和解密。
- 速度:通常比非对称加密算法快得多,因为对称加密算法的数学复杂度较低。
- 密钥分发:需要安全地分发密钥,因为密钥泄露会导致数据安全性受损。
- 密钥管理:密钥数量相对较少,管理起来比较简单,但需要确保密钥的安全。
非对称加密算法:
- 密钥特点:使用一对密钥(公钥和私钥),公钥用于加密,私钥用于解密。
- 速度:通常比对称加密算法慢,因为非对称加密算法的数学复杂度较高。
- 密钥分发:不需要安全地分发私钥,公钥可以公开分发,私钥保持在服务端。
- 密钥管理:需要管理一对密钥,但公钥可以公开,私钥需要严格保护。
其中,关键的加密工具类如下:
/** * MinIO 文件加密上传工具类 */
@Slf4j
@Component
public class MinioEncryptionUtil {
//Minio服务所在地址private String endpoint="xxxxxxxx";
//访问的keyprivate String accessKey="xxxxxxxx";
//访问的秘钥private String secretKey="xxxxxxxx";
//存储桶名称private String bucketName="filesystem-cloud";
private final String BUCKET_NAME = bucketName;
private static final String FIXED_SECRET_KEY= "xxxxxxxx"; // 固定密钥private static final String AES_ALGORITHM= "AES/ECB/PKCS5Padding";
static Long decryptedFileSize= 0L;
public final MinioClient MINIO_CLIENT = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
/** * 生成 AES 固定密钥 */private static SecretKey generateAESKey1() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = new SecureRandom(FIXED_SECRET_KEY.getBytes(StandardCharsets.UTF_8));
keyGenerator.init(256, secureRandom);
return keyGenerator.generateKey();
}
/** * 生成AES固定密钥 改 部署环境下AES密钥长度不能超过128位 * @return* @throwsException */private static SecretKey generateAESKey() throws Exception {
byte[] keyBytes = FIXED_SECRET_KEY.getBytes(StandardCharsets.UTF_8);
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] key = sha.digest(keyBytes); // 使用SHA-256哈希函数处理密钥SecretKeySpec secretKeySpec = new SecretKeySpec(key, 0, 16, "AES"); // 使用处理后的密钥字节创建SecretKeySpecreturn secretKeySpec;
}
/** * AES 加密文件 */private static byte[] encryptFile(byte[] fileData, SecretKey secretKey) throws Exception {
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(fileData);
}
/** * AES 解密文件 * * @paramencryptedData 加密后的字节数组 * @paramsecretKey AES 固定密钥 * @return解密后的字节数组 */private static byte[] decryptFile(byte[] encryptedData, SecretKey secretKey) throws Exception {
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedData);
}
/** * 批量加密并上传文件到 MinIO,返回 `List<UploadFileResult>` * @paramuploadFileDTO 需要上传的文件信息 * @returnList<UploadFileResult>
*/public List<UploadFileResult> encryptAndUpload(HttpServletRequest httpServletRequest, UploadFileDTO uploadFileDTO) {
List<UploadFileResult> uploadFileResultList = new ArrayList<>();
try {
// **转换 request 为 Multipart 请求**StandardMultipartHttpServletRequest request = (StandardMultipartHttpServletRequest) httpServletRequest;
// **检查是否是 Multipart 请求**if (!ServletFileUpload.isMultipartContent(request)) {
throw new UploadException("未包含文件上传域");
}
// **遍历所有上传文件**Iterator<String> fileNamesIterator = request.getFileNames();
while (fileNamesIterator.hasNext()) {
List<MultipartFile> multipartFileList = request.getFiles(fileNamesIterator.next());
for (MultipartFile multipartFile : multipartFileList) {
UploadFileResult uploadFileResult = new UploadFileResult();
try {
if (multipartFile.isEmpty()) {
log.error("上传文件为空: {}", multipartFile.getOriginalFilename());
continue;
}
log.info("收到文件: {}, 大小: {} bytes", multipartFile.getOriginalFilename(), multipartFile.getSize());
// **从 request 直接获取输入流**InputStream fileInputStream = multipartFile.getInputStream();
// **生成 AES 加密密钥(固定密钥)**SecretKey secretKey = generateAESKey();
// **读取文件内容并加密**byte[] fileData = readInputStreamToBytes(fileInputStream);
byte[] encryptedData = encryptFile(fileData, secretKey);
ByteArrayInputStream encryptedInputStream = new ByteArrayInputStream(encryptedData);
// **设置加密后的文件大小**uploadFileResult.setFileSize(encryptedData.length);
System.out.println("加密之后文件大小:"+encryptedData.length);
// 目标文件夹名称String targetFolder = "upload";
// 生成 MinIO 存储路径,生成文件名:目标文件夹 + 日期 + UUID + 原始文件名String encryptedFileName = targetFolder + "/" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + "/" + multipartFile.getOriginalFilename() ;
// **上传到 MinIO**MINIO_CLIENT.putObject(
PutObjectArgs.builder()
.bucket(BUCKET_NAME)
.object(encryptedFileName)
.stream(encryptedInputStream, encryptedData.length, -1)
.contentType("application/octet-stream")
.build()
);
log.info("文件成功加密并上传至 MinIO: {}", multipartFile.getOriginalFilename());
uploadFileResult.setFileUrl(encryptedFileName);
log.info(uploadFileResult.getFileUrl());
uploadFileResult.setFileName(multipartFile.getOriginalFilename());
uploadFileResult.setExtendName(StringUtils.getFilenameExtension(multipartFile.getOriginalFilename()));
uploadFileResult.setStorageType(StorageTypeEnum.MINIO);
uploadFileResult.setIdentifier(uploadFileDTO.getIdentifier());
uploadFileResult.setStatus(UploadFileStatusEnum.SUCCESS);
// **处理图片类型文件**if (UFOPUtils.isImageFile(multipartFile.getOriginalFilename())) {
try (InputStream imageStream = multipartFile.getInputStream()) {
if (imageStream.available() > 0) {
BufferedImage src = ImageIO.read(imageStream);
uploadFileResult.setBufferedImage(src);
log.info("成功解析图片: {}", multipartFile.getOriginalFilename());
}
} catch (IOException e) {
log.error("图片解析失败: {}", multipartFile.getOriginalFilename(), e);
}
}
} catch (Exception e) {
log.error("加密或上传文件失败: {}", multipartFile.getOriginalFilename(), e);
uploadFileResult.setStatus(UploadFileStatusEnum.UNCOMPLATE);
}
uploadFileResultList.add(uploadFileResult);
}
}
} catch (Exception e) {
log.error("解析上传文件失败", e);
}
return uploadFileResultList;
}
private static byte[] readInputStreamToBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[4096]; // 4KB 缓冲区int bytesRead;
while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}
return buffer.toByteArray();
}
/** * 获取 MinIO 存储的加密文件流并解密 * * @paramdownloadFile 需要下载的文件信息 * @return解密后的 `InputStream` */public InputStream getDecryptedInputStream(DownloadFile downloadFile) {
InputStream encryptedInputStream = null;
ByteArrayOutputStream decryptedOutputStream = new ByteArrayOutputStream();
try {
// **1. 从 MinIO 获取加密的文件流**if (downloadFile.getRange() != null) {
encryptedInputStream = MINIO_CLIENT.getObject(
GetObjectArgs.builder()
.bucket(BUCKET_NAME)
.object(downloadFile.getFileUrl())
.offset(downloadFile.getRange().getStart())
.length((long)downloadFile.getRange().getLength())
.build()
);
} else {
encryptedInputStream = MINIO_CLIENT.getObject(
GetObjectArgs.builder()
.bucket(BUCKET_NAME)
.object(downloadFile.getFileUrl())
.build()
);
}
if (encryptedInputStream == null) {
log.error("无法获取加密文件流: {}", downloadFile.getFileUrl());
return null;
}
log.info("成功从 MinIO 获取加密文件流: {}", downloadFile.getFileUrl());
// **2. 读取加密文件流为 byte[]**byte[] encryptedData = readInputStreamToBytes(encryptedInputStream);
// **3. 生成 AES 解密密钥(固定密钥)**SecretKey secretKey = generateAESKey();
// **4. 解密数据**byte[] decryptedData = decryptFile(encryptedData, secretKey);
// **5. 返回解密后的 InputStream**decryptedFileSize=(long)decryptedData.length;
System.out.println("解密之后文件大小:"+decryptedFileSize);
return new ByteArrayInputStream(decryptedData);
} catch (Exception e) {
log.error("解密 MinIO 文件失败: {}", downloadFile.getFileUrl(), e);
return null;
} finally {
IOUtils.closeQuietly(encryptedInputStream);
IOUtils.closeQuietly(decryptedOutputStream);
}
}
public void download(HttpServletResponse response, DownloadFile downloadFile) {
InputStream decryptedInputStream = getDecryptedInputStream(downloadFile);
response.setContentLengthLong(decryptedFileSize);
ServletOutputStream outputStream = null;
try {
if (decryptedInputStream == null) {
log.error("文件解密失败或文件不存在: {}", downloadFile.getFileUrl());
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
outputStream = response.getOutputStream();
IOUtils.copyLarge(decryptedInputStream, outputStream);
outputStream.flush();
} catch (IOException e) {
log.error("文件下载失败: {}", downloadFile.getFileUrl(), e);
} finally {
IOUtils.closeQuietly(decryptedInputStream);
IOUtils.closeQuietly(outputStream);
}
}
}