《使用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;
}
@Override
public void createBucket(String name) throws OssException {
try {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
} catch (Exception e) {
throw new OssException(e);
}
}
@Override
public 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);
}
}
@Override
public 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);
}
}
@Override
public 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);
}
}
@Override
public 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);
}
}
@Override
public 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;
}
@Override
public void createBucket(String name) throws OssException {
CreateBucketRequest createBucketRequest = new CreateBucketRequest(ossConfig.getBucketName());
try {
client.createBucket(createBucketRequest);
} catch (Exception e) {
throw new OssException(e);
}
}
@Override
public 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);
}
}
@Override
public InputStream getObject(String fileName) throws OssException {
return null;
}
@Override
public void remove(String path) throws OssException {
try {
client.deleteObject(ossConfig.getBucketName(), path);
} catch (Exception e) {
throw new OssException(e);
}
}
@Override
public 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);
}
}
@Override
public 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 {
@Autowired
private OssConfig ossConfig;
@Bean
public 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 {
@Autowired
private OssConfig ossConfig;
@Bean
public 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);
// 输出无效ID
System.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限制浏览器访问资源,只是提高了资源安全的系数,只能针对小白,懂点相关知识的人员可以轻易破解,这个问题无法从根本上解决。