最近遇到一个需求,需要给我们的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