对接华为云OBS加密存储

jasmine 于 2023-03-02 发布

1.背景

公司要求上传华为云的文件必须加密存放

2.相关文档

对接文档:https://support.huaweicloud.com/sdk-java-devg-obs/obs_21_0603.html

加密SDK:https://support.huaweicloud.com/sdk-java-devg-obs/obs_21_2303.html

官网示例:

import com.obs.services.crypto.CTRCipherGenerator;
import com.obs.services.crypto.CryptoObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.GetObjectRequest;
import com.obs.services.model.ObsObject;
import com.obs.services.model.PutObjectResult;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import org.junit.Test;
import org.m.util.MyUtil;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = ObsServiceTest.class)
public class ObsServiceTest {

    @Test
    public void test() {
        // 您可以通过环境变量获取访问密钥AK/SK,也可以使用其他外部引入方式传入。如果使用硬编码可能会存在泄露风险。
        // 您可以登录访问管理控制台获取访问密钥AK/SK
//        String ak = System.getenv("ACCESS_KEY_ID");
//        String sk = System.getenv("SECRET_ACCESS_KEY_ID");
        String ak = "RD2JV*****JB23";
        String sk = "FC3JLpTgZc*******2rHxn9LPMtNOpLb4q";
        // 【可选】如果使用临时AK/SK和SecurityToken访问OBS,同样建议您尽量避免使用硬编码,以降低信息泄露风险。
        // 您可以通过环境变量获取访问密钥AK/SK/SecurityToken,也可以使用其他外部引入方式传入。
        // String securityToken = System.getenv("SECURITY_TOKEN");
        // endpoint填写桶所在的endpoint, 此处以华北-北京四为例,其他地区请按实际情况填写。
//        String endPoint = "https://obs.cn-north-4.myhuaweicloud.com";
        String endPoint = "https://obs.cn-bj1.ctyun.cn";
        // 您可以通过环境变量获取endPoint,也可以使用其他外部引入方式传入。
        //String endPoint = System.getenv("ENDPOINT");
        CTRCipherGenerator ctrCipherGenerator = null;
        try {
            SecureRandom secureRandom = SecureRandom.getInstanceStrong();
            // 注意 Linux 下SecureRandom.getInstanceStrong()在熵不够的情况下可能阻塞,建议同时增加相关的补熵机制,或者以其他方式设置SecureRandom
            byte[] exampleMasterKey = new byte[CTRCipherGenerator.CRYPTO_KEY_BYTES_LEN];
            secureRandom.nextBytes(exampleMasterKey);
            // 这里的exampleMasterKey用随机生成的主密钥演示,使用时请您保证密钥长度为32,并妥善保管密钥。
            ctrCipherGenerator =
//                    new CTRCipherGenerator("example_master_key_info", exampleMasterKey, true, secureRandom);
                    new CTRCipherGenerator("example_master_key_info", MyUtil.getMasterKey(), true, secureRandom);
        } catch (IllegalArgumentException | NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        assert ctrCipherGenerator != null;
        // 创建ObsClient实例
        try (CryptoObsClient cryptoObsClient =
                     new CryptoObsClient(ak, sk, /*securityToken, */endPoint, ctrCipherGenerator)) {
//            String exampleBucketName = "example-bucket";
//            String exampleObjectKey = "exampleObjectKey";
            String exampleBucketName = "uy4uw3qyg3";
//            String exampleObjectKey = "dev/rrr.png";
            String exampleObjectKey = "dev/12409030000072/36673_302_ac3f6c5793b1.png";
            String examplePlainTextFilePath = "C:\\Users\\Administrator\\Downloads\\rrr.png";
            String exampleDecryptedFilePath = "C:\\Users\\Administrator\\Downloads\\36673_302_ac3f6c5793b1.png";
//            PutObjectResult putObjectResult =
//                    cryptoObsClient.putObject(exampleBucketName, exampleObjectKey, new File(examplePlainTextFilePath));
//            System.out.println("HTTP Code: " + putObjectResult.getStatusCode());
//            System.out.println("Etag: " + putObjectResult.getEtag());
            // 客户端加密上传成功
            System.out.println("CTRCipherGeneratorDemo001 putObject successfully");

            GetObjectRequest getObjectRequest = new GetObjectRequest(exampleBucketName, exampleObjectKey);
            ObsObject obsObject = cryptoObsClient.getObject(getObjectRequest);
            InputStream input = obsObject.getObjectContent();
            byte[] b = new byte[1024];
            FileOutputStream fileOutputStream = new FileOutputStream(exampleDecryptedFilePath);
            int len;
            while ((len = input.read(b)) != -1) {
                fileOutputStream.write(b, 0, len);
            }
            fileOutputStream.close();
            input.close();

            System.out.println("HTTP Code: " + obsObject.getMetadata().getStatusCode());
            // 客户端解密下载成功
            System.out.println("CTRCipherGeneratorDemo001 getObject successfully");

            // 验证加密之前的文件和解密之后的文件是否一致
            byte[] plainTextFileSha256 = CTRCipherGenerator.getFileSha256Bytes(examplePlainTextFilePath);
            byte[] decryptedFileSha256 = CTRCipherGenerator.getFileSha256Bytes(exampleDecryptedFilePath);
            String plainTextFileSha256Base64Encoded = CTRCipherGenerator.getBase64Info(plainTextFileSha256);
            String decryptedFileSha256Base64Encoded = CTRCipherGenerator.getBase64Info(decryptedFileSha256);
            System.out.println("plainTextFileSha256 base64 encoded: " + plainTextFileSha256Base64Encoded);
            System.out.println("decryptedFileSha256 base64 encoded: " + decryptedFileSha256Base64Encoded);
            System.out.println(
                    "plainTextFileSha256 equals decryptedFileSha256 ? "
                            + decryptedFileSha256Base64Encoded.equals(plainTextFileSha256Base64Encoded));
            System.out.println("CTRCipherGeneratorDemo001 successfully");
        } catch (ObsException e) {
            System.out.println("CTRCipherGeneratorDemo001 failed");
            // 请求失败,打印http状态码
            System.out.println("HTTP Code: " + e.getResponseCode());
            // 请求失败,打印服务端错误码
            System.out.println("Error Code:" + e.getErrorCode());
            // 请求失败,打印详细错误信息
            System.out.println("Error Message: " + e.getErrorMessage());
            // 请求失败,打印请求id
            System.out.println("Request ID:" + e.getErrorRequestId());
            System.out.println("Host ID:" + e.getErrorHostId());
        } catch (Exception e) {
            System.out.println("CTRCipherGeneratorDemo001 putObject failed");
            // 其他异常信息打印
            e.printStackTrace();
        }
    }
}

3.对接实战

3.1 引入依赖

<!-- 华为云存储 -->
<dependency>
    <groupId>com.huaweicloud</groupId>
    <artifactId>esdk-obs-java</artifactId>
    <version>3.24.8</version>
</dependency>

3.2 配置文件

hwyun: # 华为云OBS配置信息
   obs:
      accessKey: RD2J*********B23
      securityKey: FC3***********LPMtNOpLb4q
      endPoint: obs.cn-bj1.ctyun.cn
      bucketName: uy****qyg3
      rootPath: dev/

3.3 上传下载service

import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.List;

public interface HweiYunOBSService {

    /**
     * @Description 删除文件
     * @param: objectKey 文件名
     * @return: boolean 执行结果
     */
    boolean delete(String objectKey);

    /**
     * @Description 批量删除文件
     * @param: objectKeys 文件名集合
     * @return: boolean 执行结果
     */
    boolean delete(List<String> objectKeys);

    /**
     * @Description 上传文件
     * @param: uploadFile 上传文件
     * @param: objectKey 文件名称
     * @return: java.lang.String url访问路径
     */
    String fileUpload(MultipartFile uploadFile, String objectKey);

    /**
     * @Description 加密上传文件
     * @param: uploadFile 上传文件
     * @param: objectKey 文件名称
     * @return: java.lang.String url访问路径
     */
    String fileCryptoUpload(MultipartFile uploadFile, String objectKey);

    /**
     * @Description 文件下载
     */
    InputStream fileDownload(String objectKey);

    /**
     * @Description 文件解密下载
     */
    InputStream fileCryptoDownload(String objectKey);

    /**
     * @Description 生成ObjectKey
     */
    String generateObjectKey(String fileName);
}

3.4 实现类

import com.obs.services.ObsClient;
import com.obs.services.crypto.CryptoObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.*;
import lombok.extern.slf4j.Slf4j;
import org.mkk.boot.commontypemodeltool.exception.MKException;
import org.mkk.hpcvc.obs.properties.HweiOBSProperties;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

@Slf4j
public class HweiYunOBSServiceImpl implements HweiYunOBSService {

    private ObsClientService obsClientService;
    private HweiOBSProperties hweiOBSProperties;


    public HweiYunOBSServiceImpl(ObsClientService obsClientService, HweiOBSProperties hweiOBSProperties) {
        this.obsClientService = obsClientService;
        this.hweiOBSProperties = hweiOBSProperties;
    }

    @Override
    public boolean delete(String objectKey) {
        ObsClient obsClient = null;
        try {
            // 创建ObsClient实例
            obsClient = obsClientService.getInstance();
            // obs删除
            obsClient.deleteObject(hweiOBSProperties.getBucketName(), objectKey);
        } catch (ObsException e) {
            log.error("obs删除保存失败", e);
        } finally {
            obsClientService.destroy(obsClient);
        }
        return true;
    }

    @Override
    public boolean delete(List<String> objectKeys) {
        ObsClient obsClient = null;
        try {
            obsClient = obsClientService.getInstance();
            DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(hweiOBSProperties.getBucketName());
            objectKeys.forEach(x -> deleteObjectsRequest.addKeyAndVersion(x));
            // 批量删除请求
            obsClient.deleteObjects(deleteObjectsRequest);
            return true;
        } catch (ObsException e) {
            log.error("obs删除保存失败", e);
        } finally {
            obsClientService.destroy(obsClient);
        }
        return false;
    }

    @Override
    public String fileUpload(MultipartFile uploadFile, String objectKey) {
        ObsClient obsClient = null;
        try {
            String bucketName = hweiOBSProperties.getBucketName();
            obsClient = obsClientService.getInstance();
            // 判断桶是否存在
            boolean exists = obsClient.headBucket(bucketName);
            if (!exists) {
                // 若不存在,则创建桶
                HeaderResponse response = obsClient.createBucket(bucketName);
                log.info("创建桶成功" + response.getRequestId());
            }
            InputStream inputStream = uploadFile.getInputStream();
            long available = inputStream.available();
            PutObjectRequest request = new PutObjectRequest(bucketName, objectKey, inputStream);

            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(available);
            request.setMetadata(objectMetadata);
            // 设置对象访问权限为公共读
            request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);
            PutObjectResult result = obsClient.putObject(request);

            // 读取该已上传对象的URL
            log.info("已上传对象的URL" + result.getObjectUrl());
            return result.getObjectUrl();
        } catch (ObsException e) {
            log.error("obs上传失败", e);
        } catch (IOException e) {
            log.error("上传失败", e);
        } finally {
            obsClientService.destroy(obsClient);
        }
        return null;
    }

    @Override
    public String fileCryptoUpload(MultipartFile uploadFile, String objectKey) {
        CryptoObsClient obsClient = null;
        try {
            String bucketName = hweiOBSProperties.getBucketName();
            obsClient = obsClientService.getCryptoObsInstance();
            // 判断桶是否存在
            boolean exists = obsClient.headBucket(bucketName);
            if (!exists) {
                // 若不存在,则创建桶
                HeaderResponse response = obsClient.createBucket(bucketName);
                log.info("创建桶成功" + response.getRequestId());
            }
            InputStream inputStream = uploadFile.getInputStream();
            long available = inputStream.available();
            PutObjectRequest request = new PutObjectRequest(bucketName, objectKey, inputStream);

            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(available);
            request.setMetadata(objectMetadata);
            // 设置对象访问权限为公共读
            request.setAcl(AccessControlList.REST_CANNED_PUBLIC_READ);
            PutObjectResult result = obsClient.putObject(request);

            // 读取该已上传对象的URL
            log.info("已上传对象的URL" + result.getObjectUrl());
            return result.getObjectUrl();
        } catch (ObsException e) {
            log.error("obs上传失败", e);
        } catch (IOException e) {
            log.error("上传失败", e);
        } finally {
            obsClientService.destroy(obsClient);
        }
        return null;
    }

    @Override
    public InputStream fileDownload(String objectKey) {
        ObsClient obsClient = null;
        try {
            String bucketName = hweiOBSProperties.getBucketName();
            obsClient = obsClientService.getInstance();
            ObsObject obsObject = obsClient.getObject(bucketName, objectKey);
            return obsObject.getObjectContent();
        } catch (Exception e) {
            log.error("obs文件下载失败", e);
            throw new MKException("obs文件下载失败");
        } finally {
            obsClientService.destroy(obsClient);
        }
    }

    @Override
    public InputStream fileCryptoDownload(String objectKey) {
        CryptoObsClient obsClient = null;
        try {
            String bucketName = hweiOBSProperties.getBucketName();
            obsClient = obsClientService.getCryptoObsInstance();
            GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, objectKey);
            ObsObject obsObject = obsClient.getObject(getObjectRequest);
            return obsObject.getObjectContent();
        } catch (Exception e) {
            log.error("obs文件下载失败", e);
            throw new MKException("obs文件下载失败");
        } finally {
            obsClientService.destroy(obsClient);
        }
    }

    @Override
    public String generateObjectKey(String fileName) {
        return hweiOBSProperties.getRootPath() + fileName;
    }
}

3.5 获取Client服务

import com.obs.services.crypto.CTRCipherGenerator;
import com.obs.services.crypto.CryptoObsClient;
import com.obs.services.exception.ObsException;
import lombok.extern.slf4j.Slf4j;
import org.mkk.boot.commontypemodeltool.exception.MKException;
import org.mkk.hpcvc.obs.properties.HweiOBSProperties;
import org.mkk.hpcvc.util.MyUtil;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

/**
 * ObsClient 类。
 *
 * @author songjn
 * @date 2024-09-05 [songjn] 创建。
 */
@Slf4j
public class ObsClientService {

    private HweiOBSProperties hweiOBSProperties;

    public ObsClientService(HweiOBSProperties hweiOBSProperties) {
        this.hweiOBSProperties = hweiOBSProperties;
    }

    /**
     * 获取OBS客户端实例
     */
    public com.obs.services.ObsClient getInstance() {
        return new com.obs.services.ObsClient(hweiOBSProperties.getAccessKey(), hweiOBSProperties.getSecurityKey(), hweiOBSProperties.getEndPoint());
    }

    /**
     * 获取OBS加密客户端实例
     */
    public CryptoObsClient getCryptoObsInstance() {
        CTRCipherGenerator ctrCipherGenerator = null;
        try {
            ctrCipherGenerator =
                    new CTRCipherGenerator("hpcvc", MyUtil.getMasterKey(), true, SecureRandom.getInstanceStrong());
        } catch (NoSuchAlgorithmException e) {
            log.error("获取OBS加密客户端实例失败,NoSuchAlgorithmException", e);
            throw new MKException("获取OBS加密客户端实例失败");
        }

        CryptoObsClient cryptoObsClient =
                new CryptoObsClient(hweiOBSProperties.getAccessKey(),
                        hweiOBSProperties.getSecurityKey(),
                        hweiOBSProperties.getEndPoint(), ctrCipherGenerator);
        return cryptoObsClient;
    }


    /**
     * 销毁OBS客户端实例
     */
    public void destroy(com.obs.services.ObsClient obsClient) {
        try {
            obsClient.close();
        } catch (ObsException e) {
            log.error("obs执行失败", e);
        } catch (Exception e) {
            log.error("执行失败", e);
        }
    }
}

3.6 读取配置properties

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Setter
@Getter
@ConfigurationProperties(prefix = HweiOBSProperties.PREFIX)
public class HweiOBSProperties {

    public static final String PREFIX = "hwyun.obs";

    /**
     * 访问密钥AK
     */
    private String accessKey;

    /**
     * 访问密钥SK
     */
    private String securityKey;

    /**
     * 终端节点
     */
    private String endPoint;

    /**
     * 桶
     */
    private String bucketName;

    /**
     * 存储根路径
     */
    private String rootPath;

}

3.7 config配置文件

import lombok.extern.slf4j.Slf4j;
import org.mkk.hpcvc.obs.HweiYunOBSService;
import org.mkk.hpcvc.obs.HweiYunOBSServiceImpl;
import org.mkk.hpcvc.obs.ObsClientService;
import org.mkk.hpcvc.obs.properties.HweiOBSProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
@ConditionalOnProperty(prefix = HweiOBSProperties.PREFIX, name = "accessKey")
@EnableConfigurationProperties(HweiOBSProperties.class)
public class HweiOBSConfig {

    @Bean
    public ObsClientService obsClientService(HweiOBSProperties hweiOBSProperties) {
        return new ObsClientService(hweiOBSProperties);
    }

    @Bean
    public HweiYunOBSService hweiYunOBSService(ObsClientService obsClientService, HweiOBSProperties hweiOBSProperties) {
        return new HweiYunOBSServiceImpl(obsClientService, hweiOBSProperties);
    }
}

META-INF中spring.factorys中添加配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.mkk.hpcvc.obs.config.HweiOBSConfig

3.8 工具类获取密钥

/**
 * 获取MasterKey
 */
public static byte[] getMasterKey() {
    return Base64.getDecoder().decode(CommonConstant.OBS_SECRET);
}

/**
 * 长度32的密钥转换
 */
private void testMasterKey(String encodedText) {
    byte[] exampleMasterKey = new byte[32];

    SecureRandom secureRandom = new SecureRandom();
    secureRandom.nextBytes(exampleMasterKey);

    String keyString = Base64.getEncoder().encodeToString(exampleMasterKey);

    System.out.println("Generated Key String: " + keyString);

    byte[] decodedKey = Base64.getDecoder().decode(keyString);

    boolean isSame = java.util.Arrays.equals(exampleMasterKey, decodedKey);
    System.out.println("Is decoded key same as original? " + isSame);
}