文件存储加密实现方案

本篇文章包括以下内容:

  1. MinIO加密实现方案分析
  2. 使用代码实现加密的工具类

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);
        }
    }

}
点赞

当前页面评论已关闭。

隐藏
变装