欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 社会 > 云对象存储进阶

云对象存储进阶

2024/10/27 8:47:01 来源:https://blog.csdn.net/littleschemer/article/details/143060208  浏览:    关键词:云对象存储进阶

《使用Minio搭建文件服务器》一文对minio作了简单的介绍,本文为进阶学习。

1.对象存储产品介绍

目前市场上流行各种对象存储服务,诸如以下:

  • Amazon S3:亚马逊提供的服务, 是市场上最成熟的产品,拥有最大的市场份额

  • OSS:阿里巴巴提供

  • COS:腾讯云提供
  • Minio: 开源版本免费,企业版需要付费,开源版本免费,企业版需要付费

总结

  • 市场占有率:Amazon S3 是市场上最成熟的产品,拥有最大的市场份额。
  • 成本:所有服务都采用按需付费模式,但具体成本取决于使用量和具体配置。
  • 兼容性:MinIO 提供与 Amazon S3 的兼容性,这使得它可以作为私有云环境中的替代方案。
  • 部署灵活性:MinIO 提供了更高的部署灵活性,可以在本地或云环境中部署。
  • 安全性:所有服务都提供了强大的安全特性,包括数据加密和访问控制

2.兼容性api设计

MinIO 与 Amazon S3 的API是兼容的,这意味着使用minio的sdk可以访问AmazonS3,反之亦然。

从实际表示来看,只能说大部分兼容,真正用起来还是偶尔出现一些异常,例如,笔者用minio的sdk来访问docker下的minio组件,从没发生IO异常,但用来访问AmazonS3,则偶尔会出现IO异常。

网上说有可能是网络不稳定,实在找不出原因,于是就想重构一下,对于内网使用minio的sdk库访问minio组件,外网使用amazons3的sdk访问亚马逊的云存储。

针对业务上,我们对于s3的基本操作是资源的上传,删除,拷贝,获取(比较少用),而对数据桶的操作,例如创建、桶删除等,由于比较少用,就不介绍了。

2.1.API接口设计

(generatePresignedUrl方法后面再介绍)

public interface S3Client {/*** 创建数据桶* @param filePath s3内部路径*/String upload(InputStream input, String filePath, String contentType) throws OssException;/*** 获取指定对象输入流*/InputStream getObject(String fileName) throws OssException;/*** 删除对象*/void remove(String objectName) throws OssException;/*** 复制对象,只支持同一个桶内部复制(简化API)*/void copyResource(String copy, String target) throws OssException;/*** 生成客户端临时上传路径*/String generatePresignedUrl(String path) throws OssException;
}

配置项,不作过多解释

@ConfigurationProperties(prefix = "s3")
@Component
@Data
public class OssConfig {/*** 这个值对于 amazon统一为 s3.amazonaws.com,* 对于MinIO,可自定义*/private String endpoint;private String accessKey;private String secretKey;private String bucketName;/*** cdn域名,只在客户端读取文件使用,服务端上传文件统一用endpoint* 对于一些文件内容可能会被修改,则不适用cdn。因为cdn在缓存期间是不会重新刷新资源的*/private String cdn;/*** 使用minio,该值可以配置为amazon任何一个有效区域,唯一作用只是为了兼容amazonAPI*/private String region;}

2.2.minio实现(引入io.minio:minio依赖)

public class MinIoS3Client implements S3Client {private MinioClient minioClient;private OssConfig ossConfig;public MinIoS3Client(MinioClient minioClient, OssConfig ossConfig) {this.minioClient = minioClient;this.ossConfig = ossConfig;}@Overridepublic void createBucket(String name) throws OssException {try {minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());} catch (Exception e) {throw new OssException(e);}}@Overridepublic String upload(InputStream input, String filePath, String contentType) throws OssException {try {minioClient.putObject(PutObjectArgs.builder().bucket(ossConfig.getBucketName()).object(filePath).stream(input, input.available(), -1).contentType(contentType).build());return filePath;} catch (Exception e) {throw new OssException(e);}}@Overridepublic InputStream getObject(String fileName) throws OssException {GetObjectArgs request = GetObjectArgs.builder().bucket(ossConfig.getBucketName()).object(fileName).build();try {return minioClient.getObject(request);} catch (Exception e) {throw new OssException(e);}}@Overridepublic void remove(String objectName) throws OssException {RemoveObjectArgs request = RemoveObjectArgs.builder().bucket(ossConfig.getBucketName()).object(objectName).build();try {minioClient.removeObject(request);} catch (Exception e) {throw new OssException(e);}}@Overridepublic void copyResource(String copy, String target) throws OssException {try {String bucket = ossConfig.getBucketName();minioClient.copyObject(CopyObjectArgs.builder().bucket(bucket).object(target).source(CopySource.builder().bucket(bucket).object(copy).build()).build());} catch (Exception e) {throw new OssException(e);}}@Overridepublic String generatePresignedUrl(String path) throws OssException {try {return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.PUT).bucket(ossConfig.getBucketName()).object(path).expiry(1, TimeUnit.HOURS).build());} catch (Exception e) {throw new OssException(e);}}}

2.3.amazons3实现(引入com.amazonaws:aws-java-sdk-s3依赖)

public class AmazonS3Client implements S3Client {private AmazonS3 client;private OssConfig ossConfig;public AmazonS3Client(AmazonS3 client, OssConfig ossConfig) {this.client = client;this.ossConfig = ossConfig;}@Overridepublic void createBucket(String name) throws OssException {CreateBucketRequest createBucketRequest = new CreateBucketRequest(ossConfig.getBucketName());try {client.createBucket(createBucketRequest);} catch (Exception e) {throw new OssException(e);}}@Overridepublic String upload(InputStream input, String filePath, String contentType) throws OssException {try {ObjectMetadata metadata = new ObjectMetadata();metadata.setContentType(contentType);metadata.setContentLength(input.available());PutObjectRequest putObjectRequest = new PutObjectRequest(ossConfig.getBucketName(), filePath, input, metadata);client.putObject(putObjectRequest);return filePath;} catch (Exception e) {throw new OssException(e);}}@Overridepublic InputStream getObject(String fileName) throws OssException {return null;}@Overridepublic void remove(String path) throws OssException {try {client.deleteObject(ossConfig.getBucketName(), path);} catch (Exception e) {throw new OssException(e);}}@Overridepublic void copyResource(String copy, String target) throws OssException {try {CopyObjectRequest request = new CopyObjectRequest().withSourceBucketName(ossConfig.getBucketName()).withSourceKey(copy) // 源对象键(文件名).withDestinationBucketName(ossConfig.getBucketName()).withDestinationKey(target); // 目标对象client.copyObject(request);} catch (Exception e) {throw new OssException(e);}}@Overridepublic String generatePresignedUrl(String path) throws OssException {long expiration = System.currentTimeMillis() + TimeUtil.MILLIS_PER_HOUR;try {GeneratePresignedUrlRequest request =new GeneratePresignedUrlRequest(ossConfig.getBucketName(), path).withMethod(HttpMethod.PUT).withExpiration(new Date(expiration));return client.generatePresignedUrl(request).toString();} catch (Exception e) {throw new OssException(e);}}
}

2.4.利用springboot的condition机制作配置切换

spring自4.X提供了Condition条件机制,springboot在此基本上实现了一系列ConditionOnXXX注解。我们可以用来实现,当配置s3.type="minio",启动minio客户端;当配置s3.type="amazon",启动amazon客户端。代码如下:

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 io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class OssAutoConfig {@ConditionalOnProperty(name = "s3.type", havingValue = "minio")public static class MinioAutoConfig {@Autowiredprivate OssConfig ossConfig;@Beanpublic S3Client createS3Client() throws Exception {MinioClient minioClient = minioClient();return new MinIoS3Client(minioClient, ossConfig);}private MinioClient minioClient() throws Exception {return MinioClient.builder().endpoint(ossConfig.getEndpoint()).region(ossConfig.getRegion()).credentials(ossConfig.getAccessKey(), ossConfig.getSecretKey()).build();}}@ConditionalOnProperty(name = "s3.type", havingValue = "amazon")public static class AmazonAutoConfig {@Autowiredprivate OssConfig ossConfig;@Beanpublic S3Client createS3Client() throws Exception {AWSCredentials credentials = new BasicAWSCredentials(ossConfig.getAccessKey(), ossConfig.getSecretKey());AwsClientBuilder.EndpointConfiguration endpointConfig = new AwsClientBuilder.EndpointConfiguration(ossConfig.getEndpoint(), // amazon统一为s3.amazonaws.com,MinIO可自定义ossConfig.getRegion()// 签名区域,对于 MinIO 来说,这个值可以是任意字符串);AmazonS3 s3Client = AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(credentials)).withEndpointConfiguration(endpointConfig).build();return new AmazonS3Client(s3Client, ossConfig);}}
}

3. 客户端文件直达存储服务

对于文件上传,有两种机制,一种是浏览器将文件上传到服务器,再由服务器转发到s3等云对象存储。另外一种,是客户端将需要发送的文件告知服务器,服务器验证通过后,生成一个临时上传路径,客户端通过临时路径直传s3。两者各有优缺点,如下:

转发直传
优点对文件等信息进行强验证不影响服务器吞吐量
缺点浪费服务器资源,io按双倍算客户端可能会作弊,例如验证是a文件,实际发送的是b文件。资源状态可能不同步,例如服务器已保存相关资源信息,但客户端中断上传。

相关逻辑见S3Client接口

/*** 生成客户端临时上传路径*/
String generatePresignedUrl(String path) throws OssException;

4.cdn加速访问

4.1.配置cloudfront

当云对象存储与cdn(内容分发网络)结合在一起,可以极大的提供网站的性能以及用户体验。CDN通过将内容缓存到全球分布的边缘节点,可以更快地将内容传递给用户,减少延迟。

Amazon CloudFront是亚马逊提供的CDN服务,可以与AmazonS3完美结合在一起,开通CloudFront服务后,可以设置各种访问策略,cors跨域策略,资源防盗策略等等。

如下的s3配置

需要注意两个地方:

1.亚马逊是全球服务,endpoint是固定的,如果采用minio,则采用部署的实际地址。region是一个区域列表,代表s3服务的部署区域,如果采用minio,则随便选一个。

2.cdn是针对桶进行建立的,如果使用了cdn地址,则返回给客户端的资源路径无需带上桶名称。

4.2.清除缓存

由于CDN自带缓存机制,意味着浏览器从cdn拿到数据之后,只要不过期(过期时间可设置)就不会重新请求。但如果你的资源是可变的,例如json文件,html代码,cdn反而导致你的更新无法被所有人感知。因此,对于内容可能发生变化的资源,可以选择使用S3作为原始地址

或者使用cloudfrone的sdk,手动触发缓存失效。

首先,引入sdk(好像还有2.x版本)

  <dependency><groupId>com.amazonaws</groupId><artifactId>aws-java-sdk-cloudfront</artifactId><version>1.12.540</version></dependency>

示例代码

 // 创建CloudFront客户端// 使用您的AWS访问密钥和秘密密钥初始化凭证对象String accessKey = "xxxx";String secretKey = "yyyyyyyyyyyyy";AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);AmazonCloudFrontClient cloudFrontClient = new AmazonCloudFrontClient(credentials);String distributionId = "zzzzzzzzz";// 创建无效项请求Paths paths = new Paths();paths.withItems("database/picture/cf1eed1fcd6f42bb8f42de725da374e3.jpg");InvalidationBatch invalidationBatch = new InvalidationBatch().withPaths(paths);// 请求流水号,同一个流水号,cdn服务器只会执行一次invalidationBatch.withCallerReference("" + System.currentTimeMillis());// 创建无效请求CreateInvalidationRequest createInvalidationRequest = new CreateInvalidationRequest(distributionId, invalidationBatch);// 发送无效请求并获取结果CreateInvalidationResult createInvalidationResult = cloudFrontClient.createInvalidation(createInvalidationRequest);// 输出无效IDSystem.out.println("Invalidation ID: " + createInvalidationResult.getInvalidation().getId());

5.资源防盗

一些平台网站会限制只允许自己的网站访问内容资源(特别是以资源作卖点的产品),不允许用户直接通过浏览器访问资源(包括但不限于图片,视频等)。例如下面的图片链接,直接在浏览器上进行访问,会报错(403代表服务器禁止回应)。CDN文章的图片没有如此限制,随便访问。

5.1原理

图片等大部分资源都是放在s3类产品,其禁止资源防盗的原理,一般都是通过http请求的Referer参数(由浏览器自行注入),Referer代表当前访问的页面(区别于origin请求头),如果Referer的域名不在s3配置的域名列表,则不允许访问。

理解该原理之后,破解该策略也就很容易了。我们只需把正常访问里的http请求头带到自己的http请求即可(任何能发起http请求的工具Curl或编程语言都可以)。

5.2.python带上header绕过检测

import requests
from PIL import Image
from io import BytesIO
def query_sth():url = '此处填写目标地址'# 定义你的header信息headers = {'method': 'GET','scheme': 'https','Accept-Encoding': 'gzip, deflate, br, zstd','Accept-Language': 'zh-CN,zh;q=0.9','Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8','Referer': '浏览器正常访问的referer参数','Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"','Sec-Ch-Ua-Mobile': '?0','Sec-Ch-Ua-Platform': '"Windows"','Sec-Fetch-Dest': 'empty','Sec-Fetch-Mode': 'cors','Sec-Fetch-Site': 'same-site','User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',}# 发送请求response = requests.get(url, headers=headers)if response.status_code == 200:# 获取二进制图像数据image_data = response.content# print(response.content)# 使用BytesIO创建一个文件类对象image_stream = BytesIO(image_data)# 使用Pillow打开图像image = Image.open(image_stream)# 显示图像image.show()# 如果你想保存图像到本地# image.save('local_image.jpg')else:print('Failed to retrieve image')print(response)query_sth()

总结:

通过referer限制浏览器访问资源,只是提高了资源安全的系数,只能针对小白,懂点相关知识的人员可以轻易破解,这个问题无法从根本上解决。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com