【橘子场景问题】你的S3文件存储需要加密吗

最近遇到一个需求,需要给我们的minio集群上传的文件进行加密,领导给了我一周调研+开发,哥一想这也要五天?看来又能有三天时间摸鱼了,于是上去就是一顿百度,找到了一篇文章,上去就是一顿"借鉴"。

一、单文件上传加密

文章来源在这里https://blog.youkuaiyun.com/wangxudongx/article/details/130451389
于是我用了三分钟改完了,测试没问题。于是就开始刷起了贴吧。
然后麻烦来了,和我对接的小伙伴说,我们这个是大文件上传,前端是切片上传的。于是我麻了,我总不能每个切片都去对流文件加密,然后最后合并流文件吧,这特么出来的是个啥啊。
而且目前我们的实现是前端直接上传server服务,最后调用s3的merge api对已经罗盘的数据进行合并。也就是说不经过我的后端代码,我都没法加密。总不能我再拉下来再内存里面做加密吗,这不仅危险而且low。
但是我们想一想,数据加密,这是个非常普遍的需求,官方肯定支持的。于是啥也不说了,开始调研。

二、KMS加密

我废话不多说,原理和要求都在文档。
S3服务KMS加密数据安全
简单来说就是我们只要配置了这个加密方式和策略,在你上传上去之后,他就能对你的数据自动加密,当然你也可以在客户端不指定加密。不需要我们去对数据做处理。
至于分片合并,他本身就支持,所以这个是很完美的解决方案。
于是就开始了处理。

1、安装valut服务

这个服务就是支持KMS的,也就是你的数据最后会被他处理。安装 vault服务的时候会生成token和key,处理加密解密工作。操作文档如下:
vault官方文档

1、sudo yum install -y yum-utils

2、sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo

3、sudo yum -y install vault

# 启动vault
4、vault server -dev

记住启动之后控制台生成的token和key,后面需要使用。
在这里插入图片描述

2、minio配置https访问

2.1、生成公私钥

下载生成工具地址

# 这里就是指定你服务端的域名或者ip地址,可以传入多个
certgen -host "minio服务器地址,127.0.0.1,localhost"

Created a new certificate 'public.crt', 'private.key' valid for the following names 📜
 - "127.0.0.1"
 - "localhost"

产生两个文件private.key public.crt,放到/{$HOME}/.minio/certs下,默认为root/.minio/certs

2.2、配置minio配置文件

配置文件默认位于/{$HOME}/.minio下的config.json文件中,我的位置为/root/.minio/config.json。没有就创建一个。

{
  "port": 9000,
  "certificates": [
    {
      "enable_tls": true, # 开启tls
      "certificate": "/root/.minio/certs/public.crt", # 公钥位置
      "key": "/root/.minio/certs/private.key"  # 私钥位置
    }
  ],
  "vault": {   # 配置kms
    "enabled": true,
    "address": "http://127.0.0.1:8200", # vault服务地址端口
    "token": "hvs.UNGHZnU2O7CnllgKgFAFLtpK", # 安装vault服务生成的token
    "key": "cIJKWj//MZU/7DjremYC8TKfMux/TBerU3s+95Px2vg=" # 安装vault服务生成的key
  },
  "MINIO_VOLUMES": "https://172.16.102.230:9000/data"
}

2.3、指定配置文件方式重启Minio服务

# --config-dir /root/.minio 为指定配置文件,--config-dir后跟你的配置文件所在目录即可
./bin/minio server --config-dir /root/.minio /u01/isi/application/component/minio/data >> /u01/isi/application/component/minio/logs/minio.log

3、对JDK服务进行秘钥配置

此时服务配置为https访问,jdk对于未知的安全地址会拒绝,抛出异常:PKIX path building failed。
需要把刚才生成的秘钥证书crt文件加入到jdk的密钥库中。
进入到jdk的bin目录中,执行如下命令。实际生产需要集成到dockerfile中。

# keytool 是 Java 中用于管理密钥和证书的工具。-import 参数用于将证书从其他密钥库导入到 Java 密钥库中。以下是 -import 参数的常用选项:
# -alias <alias>:为导入的密钥指定别名。
# -file <filename>:指定要导入的证书文件名。
# -keystore <keystore-name>:指定密钥库的名称(默认为 .keystore)。
# -keypass <password>:指定密钥的密码(默认为 changeit)。
# -storepass <password>:指定密钥库的密码(默认为 changeit)。
# -noprompt:不提示输入密钥库密码。
# -trustcacerts:将 CA 证书导入信任的 CA 证书列表。
# -cacerts:指定 CA 证书的密钥库(默认为 cacerts)

keytool -import  -v -trustcacerts -alias s3crt  -file  [crt的路径]  -storepass changeit -keystore "C:\Users\DELL\.jdks\jbr-17.0.9\lib\security\cacerts"
其中C:\Users\DELL\.jdks\jbr-17.0.9为jdk安装目录,cacerts表示放到jdk的security下的cacerts密钥库。

例子如下:
keytool -import  -v -trustcacerts -alias s3crt  -file  C:\Users\DELL\.jdks\jbr-17.0.9\lib\security\public.crt  -storepass changeit -keystore "C:\Users\DELL\.jdks\jbr-17.0.9\lib\security\cacerts"

在这里插入图片描述
在这里插入图片描述
并且在代码中设置参数告知JVM访问密钥库。下面为写在代码中,实际生产可以在dockerfile中写入jvm参数

System.setProperty("javax.net.ssl.trustStore", "cacerts");	// cacerts为秘钥库

实际可以在jvm启动的时候以参数形式传入启动,无需写死在代码中:java -jar -Djavax.net.ssl.trustStore=cacerts app.jar

以上过程最好都在root用户下执行,注意读写权限。

4、代码实现

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-s3</artifactId>
    <version>1.12.572</version>
</dependency>
import lombok.Data;

@Data
public class S3Config {
    private String accessKey;
    private String secretKey;
    private String bucket;
    private String endpoint;
    private String domain;
    private String region;
    private String service;
}

import com.amazonaws.HttpMethod;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.springframework.web.multipart.MultipartFile;

import xyz.playedu.common.exception.ServiceException;
import xyz.playedu.common.types.config.S3Config;
import xyz.playedu.common.types.config.S3KeyConfig;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Slf4j
public class S3Util {

    private S3Config defaultConfig;

    private S3KeyConfig s3KeyConfig;

    public S3KeyConfig getS3KeyConfig() {
        return s3KeyConfig;
    }

    public void setS3KeyConfig(S3KeyConfig s3KeyConfig) {
        this.s3KeyConfig = s3KeyConfig;
    }

    public S3Config getS3Config() {
        return defaultConfig;
    }

    public S3Util(S3Config s3Config) {
        defaultConfig = s3Config;
    }

    public S3Util setConfig(S3Config config) {
        defaultConfig = config;
        return this;
    }

    public boolean configIsEmpty() {
        return defaultConfig == null
                || StringUtil.isEmpty(defaultConfig.getDomain())
                || StringUtil.isEmpty(defaultConfig.getEndpoint())
                || StringUtil.isEmpty(defaultConfig.getAccessKey())
                || StringUtil.isEmpty(defaultConfig.getSecretKey());
    }

    @SneakyThrows
    private AmazonS3 getClient() {
        if (defaultConfig == null) {
            throw new ServiceException("存储服务未配置");
        }
        AWSCredentials credentials =
                new BasicAWSCredentials(defaultConfig.getAccessKey(), defaultConfig.getSecretKey());

        AwsClientBuilder.EndpointConfiguration endpointConfiguration =
                new AwsClientBuilder.EndpointConfiguration(
                        defaultConfig.getEndpoint(), defaultConfig.getRegion());

        AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
        // 开启路径访问
        builder.setPathStyleAccessEnabled(true);

        return builder.withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(endpointConfiguration)
                .build();
    }

    @SneakyThrows
    public String saveFile(MultipartFile file, String savePath, String contentType) {
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        objectMetadata.setContentLength(file.getInputStream().available());
        getClient()
                .putObject(
                        defaultConfig.getBucket(), savePath, file.getInputStream(), objectMetadata);
        return generateEndpointPreSignUrl(savePath);
    }

    @SneakyThrows
    public String saveBytes(byte[] file, String savePath, String contentType) {
        InputStream inputStream = new ByteArrayInputStream(file);
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        objectMetadata.setContentLength(inputStream.available());
        getClient().putObject(defaultConfig.getBucket(), savePath, inputStream, objectMetadata);
        return generateEndpointPreSignUrl(savePath);
    }

    public String uploadId(String path) {
        InitiateMultipartUploadRequest request =
                new InitiateMultipartUploadRequest(defaultConfig.getBucket(), path);
        InitiateMultipartUploadResult result = getClient().initiateMultipartUpload(request);
        return result.getUploadId();
    }

    public String generatePartUploadPreSignUrl(
            String filename, String partNumber, String uploadId) {
        GeneratePresignedUrlRequest request =
                new GeneratePresignedUrlRequest(
                        defaultConfig.getBucket(), filename, HttpMethod.PUT);
        request.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)); // 一个小时有效期
        request.addRequestParameter("partNumber", partNumber); // 分块索引
        request.addRequestParameter("uploadId", uploadId); // uploadId
        return getClient().generatePresignedUrl(request).toString();
    }

    @SneakyThrows
    public String merge(String filename, String uploadId) {
        AmazonS3 client = getClient();

        ListPartsRequest listPartsRequest =
                new ListPartsRequest(defaultConfig.getBucket(), filename, uploadId);
        PartListing parts = client.listParts(listPartsRequest);
        if (parts.getParts().isEmpty()) {
            throw new ServiceException("没有已上传的分片文件");
        }

        List<PartETag> eTags = new ArrayList<>();
        parts.getParts()
                .forEach(
                        item -> {
                            eTags.add(new PartETag(item.getPartNumber(), item.getETag()));
                        });

        CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest();
        request.setBucketName(defaultConfig.getBucket());
        request.setKey(filename);
        request.setUploadId(uploadId);
        request.setPartETags(eTags);

        client.completeMultipartUpload(request);
        String s3Key = s3KeyConfig.getS3Key();
        if (StringUtil.isNotEmpty(s3Key)) {
            SSECustomerKey sseCustomerKey = new SSECustomerKey(s3Key);
            request.setSSECustomerKey(sseCustomerKey);
        }
        return generateEndpointPreSignUrl(filename);
    }

    public void removeByPath(String path) {
        DeleteObjectRequest request = new DeleteObjectRequest(defaultConfig.getBucket(), path);
        getClient().deleteObject(request);
    }

    public boolean exists(String path) {
        return getClient().doesObjectExist(defaultConfig.getBucket(), path);
    }

    @SneakyThrows
    public String getContent(String path) {
        S3Object s3Object = getClient().getObject(defaultConfig.getBucket(), path);
        return new String(s3Object.getObjectContent().readAllBytes(), StandardCharsets.UTF_8);
    }

    public String generateEndpointPreSignUrl(String path) {
        if (defaultConfig.getService().equals("minio")) {
            return defaultConfig.getDomain() + "/" + path;
        }
        return "";
    }


    @SneakyThrows
    public String saveFile2(MultipartFile file, String savePath, String contentType) {
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        objectMetadata.setContentLength(file.getInputStream().available());
        getClient()
                .putObject(
                        defaultConfig.getBucket(), savePath, file.getInputStream(), objectMetadata);
        return generateEndpointPreSignUrl(savePath);
    }
}

4.1、上传数据

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.PutObjectResult;
import com.amazonaws.services.s3.model.SSECustomerKey;
import xyz.playedu.common.types.config.S3Config;
import xyz.playedu.common.types.config.S3KeyConfig;
import xyz.playedu.common.util.S3Util;

import java.io.File;

public class TestUpload {
    public static void main(String[] args) {
        // 告知jdk识别证书
        System.setProperty("javax.net.ssl.trustStore", "cacerts");

        S3Config defaultConfig = new S3Config();
        // 上传到minio的bucket
        defaultConfig.setBucket("test");
        // minio的账户
        defaultConfig.setAccessKey("minioadmin");
        // minio的密码
        defaultConfig.setSecretKey("miniopass");
        // minio的地址
        defaultConfig.setEndpoint("https://ip:9000");
        // minio的地址
        defaultConfig.setDomain("https://ip:9000");
        S3Util s3Util = new S3Util(defaultConfig);
        S3KeyConfig s3KeyConfig = new S3KeyConfig();
        // valuat的Key
        String s3Key = "valuat的Key";
        s3KeyConfig.setS3Key(s3Key);
        s3Util.setS3KeyConfig(s3KeyConfig);
        // 要上传的本地文件
        File file = new File("D:\\file\\levi.mp4");
        String keyName = "levi.mp4";

        AWSCredentials credentials =
                new BasicAWSCredentials(defaultConfig.getAccessKey(), defaultConfig.getSecretKey());

        AwsClientBuilder.EndpointConfiguration endpointConfiguration =
                new AwsClientBuilder.EndpointConfiguration(
                        defaultConfig.getEndpoint(), defaultConfig.getRegion());

        AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
        // 开启路径访问
        builder.setPathStyleAccessEnabled(true);

        AmazonS3 amazonS3 = builder.withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(endpointConfiguration)

                .build();

        PutObjectRequest putObjectRequest = new PutObjectRequest(defaultConfig.getBucket(), keyName, file);
        SSECustomerKey sseCustomerKey = new SSECustomerKey(s3Key);
        putObjectRequest.setSSECustomerKey(sseCustomerKey);

        PutObjectResult putObjectResult = amazonS3.putObject(putObjectRequest);
        System.out.println(putObjectResult.getSSECustomerAlgorithm());
        System.out.println(putObjectResult.getMetadata());
    }
}

运行程序,上传成功,然后打开minio对应的bucket查看,这时候你通过后台直接下载是会报失败的,加密文件了属于是。
需要通过解密才能获取。

4.2、下载数据

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import xyz.playedu.common.types.config.S3Config;
import xyz.playedu.common.types.config.S3KeyConfig;
import xyz.playedu.common.util.S3Util;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class Testget {
    public static void main(String[] args) {
        System.setProperty("javax.net.ssl.trustStore", "cacerts");	// 刚才填写的路径

        S3Config defaultConfig = new S3Config();
        defaultConfig.setBucket("test");
        defaultConfig.setAccessKey("minioadmin");
        defaultConfig.setSecretKey("miniopass");
        defaultConfig.setEndpoint("https://ip:9000");
        defaultConfig.setDomain("https://ip:9000");
        S3Util s3Util = new S3Util(defaultConfig);
        S3KeyConfig s3KeyConfig = new S3KeyConfig();
        String s3Key = "valuat的Key";
        s3KeyConfig.setS3Key(s3Key);
        s3Util.setS3KeyConfig(s3KeyConfig);
        // 下载到
        File file = new File("D:\\file\\levi.mp4");
        String keyName = "levi.mp4";

        AWSCredentials credentials =
                new BasicAWSCredentials(defaultConfig.getAccessKey(), defaultConfig.getSecretKey());

        AwsClientBuilder.EndpointConfiguration endpointConfiguration =
                new AwsClientBuilder.EndpointConfiguration(
                        defaultConfig.getEndpoint(), defaultConfig.getRegion());

        AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
        // 开启路径访问
        builder.setPathStyleAccessEnabled(true);

        AmazonS3 amazonS3 = builder.withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(endpointConfiguration)

                .build();

        GetObjectRequest getObjectRequest = new GetObjectRequest("test", keyName);
        SSECustomerKey sseCustomerKey = new SSECustomerKey(s3Key);
        getObjectRequest.setSSECustomerKey(sseCustomerKey);

        S3Object object = amazonS3.getObject(getObjectRequest);
        S3ObjectInputStream objectContent = object.getObjectContent();

        // 指定本地文件路径
        String localFilePath = "D:\\levi.mp4";

        try (OutputStream outputStream = new FileOutputStream(new File(localFilePath))) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = objectContent.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            System.out.println("文件成功输出到:" + localFilePath);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                objectContent.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4、参考链接

1、https://blog.youkuaiyun.com/liruiqing/article/details/80416740
2、https://github.com/minio/certgen
3、https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-install
4、https://blog.youkuaiyun.com/xuzhongyi103/article/details/131515281

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值