单体应用结构
对象存储在网站交互中必不可少,一般简单的实现无外乎,后端服务对接COS SDK实现对对象的存储等操作,后端服务开放接口,前端服务调用,从而实现表单的提交。整个结构类似下面这样
微服务结构
微服务架构下,有网关的加入,服务的划分,结构会变成下面这样,一个简单的上传文件流程就变得复杂起来了,上传时文件要以流的形式上传到服务中,要上传的文件变大后,对于网关,服务堆空间的考验都很大,细细想来实际上,文件上传是不应该过网关才更合理,或者有一个专门用于对象上传下载的网关(这条路涉及到的工作量显然更多)
细分文件上传需求后我们发现
- 小文件上传可以沿用之前的方案:即后端提供接口,前端直接调用并响应公网可访问url
- 针对两种大文件上传的需求
- 前端直接上传大文件获取公网可访问url
- 前端上传大文件,但是业务服务需要自己进行加密处理后再重新上传的需求
任务拆解
前端需要有直接写cos桶的能力
但是对接COS的账号不能直接存到前端,后端可以使用腾讯云getCredential()接口,给予前端临时密钥,前端通过临时密钥,直接上传到COS,拜托网关以及服务堆空间的限制
桶读写权限的限制
首先排除公读公写的权限组合
前端如果上传到公读私写,响应后的key可以通过遍历访问到全部的文件,存在安全风险
前端只能上传到私读私写桶中,响应后的key无法直接访问,需要再次通过后端服务利用COS API generatePresignedUrl()来获取临时访问url
由此有了最新的方案,前端或者业务服务通过临时密钥的方式自己对接cos桶,完成上传操作
针对需求一
前端直接上传大文件获取公网可访问url
实现
引入maven依赖
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.18</version>
</dependency>
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.18</version>
</dependency>
获取临时文件许可
CosStsClient.getCredential(TreeMap<String, Object> config)使用该方法
TreeMap中put 参数配置以及权限配置等内容
可以参考官方文档
链接: 临时密钥生成及使用指引
前提你创建treeMap的secretId以及secretKey一定要有对应桶的写权限
前端对接COS使用临时密钥上传文件
下载腾讯云js sdk
链接: js-sdk下载地址
代码样例如下
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
<style>
/* 样式用于美化上传区域 */
#upload-area {
width: 300px;
height: 200px;
border: 2px dashed #ccc;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
cursor: pointer;
}
</style>
</head>
<body>
<h1>文件上传</h1>
<div id="upload-area">
<p>拖放文件到此处或点击选择文件</p>
</div>
<script src="./cos-js-sdk-v5.min.js"></script>
<script>
// 获取上传区域元素
const uploadArea = document.getElementById('upload-area');
// 在上传区域上添加事件监听器
uploadArea.addEventListener('dragover', handleDragOver, false);
uploadArea.addEventListener('dragleave', handleDragLeave, false);
uploadArea.addEventListener('drop', handleFileSelect, false);
uploadArea.addEventListener('click', handleClick, false);
// 阻止默认拖放行为
function handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
uploadArea.style.border = '2px dashed #888';
}
// 拖离上传区域时恢复样式
function handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
uploadArea.style.border = '2px dashed #ccc';
}
// 处理文件选择
function handleFileSelect(event) {
event.preventDefault();
event.stopPropagation();
uploadArea.style.border = '2px dashed #ccc';
const files = event.dataTransfer.files; // 获取拖放的文件列表
handleFiles(files);
}
// 处理点击选择文件
function handleClick(event) {
event.preventDefault();
event.stopPropagation();
// 创建一个隐藏的 input[type="file"] 元素
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
// 监听文件选择事件
fileInput.addEventListener('change', function () {
const files = fileInput.files; // 获取选择的文件列表
handleFiles(files);
});
// 触发点击事件
fileInput.click();
}
// 处理文件列表
function handleFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
console.log('上传文件:', file.name, file.size, file.type);
// 执行上传逻辑
uploadCos({ file: file,onSuccess: (data) => {
console.log('文件上传成功:', data);
// 在这里执行上传成功后的操作
} }); // 将文件对象作为参数传入
}
}
function uploadCos(option) {
// 假设你已经有了临时凭证信息
const cos = new COS({
getAuthorization: function (options, callback) {
const credentials = {
tmpSecretId: 'xxx', // 替换为您的临时 SecretId
tmpSecretKey: 'xxx', // 替换为您的临时 SecretKey
sessionToken: 'xxx',
startTime: 临时密钥有效期起始时间戳,
expiredTime: 临时密钥有效期截止时间戳
};
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
SecurityToken: credentials.sessionToken,
StartTime: credentials.startTime,
ExpiredTime: credentials.expiredTime
});
}
});
const errFn = (err, data) => {
if (err) {
console.error('文件上传失败,请稍后重试!', err);
} else {
option.onSuccess(data);
}
};
const progressFn = (progressData) => {
if (!done) {
option.onProgress(progressData);
if (progressData.percent >= 1) {
done = true;
}
}
};
const { file = {} } = option;
let done = false;
const timestamp = new Date().getTime();
// 指定上传文件的全路径,不能不设置,否则会有既有文件被覆盖的风险
const newFileName = '文件key 这个值会在后端获取可上传文件路径时指定,依据自己的情况';
console.log('文件名:', newFileName);
cos.putObject(
{
Bucket: '自己桶的名称',
Region: '自己桶的区域',
Key: newFileName,
Body: file,
onProgress: (progressData) => progressFn(progressData),
},
(err, data) => errFn(err, data)
);
}
</script>
</body>
</html>
通知上传完成
该接口内部操作即为:通过cosObjectKey(前端上传后响应的key,可以理解为一个文件对象的键)将该对象由私读私写桶 复制到 公读私写桶
就需要使用到COS API copyObject()方法
其实就是将私读私写桶中的文件通过参数key复制到公读私写桶
文档参考链接: 复制与移动对象
自己实现的样例
// 首先要创建TransferManager对象
private TransferManager createTransferManage() {
ExecutorService threadPool = new ThreadPoolExecutor(CommonConstants.EIGHT, CommonConstants.SIX_TEEN, CommonConstants.ZERO_LONG, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(CommonConstants.ONE_THOUSAND), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
// 这里创建的 cosClient 是以复制的目的端信息为基础的
// 要特别注意 这个cosClient初始化时的账号要有两个桶的读取权限,要有目的桶的写入权限
TransferManager transferManager = new TransferManager(cosClient, threadPool);
// 分块复制阈值和分块大小分别为 40MB 和 4MB
TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration();
transferManagerConfiguration.setMultipartCopyThreshold(CommonConstants.MB40);
transferManagerConfiguration.setMultipartCopyPartSize(CommonConstants.MB4);
transferManager.setConfiguration(transferManagerConfiguration);
return transferManager;
}
// 方法的主要实现
public String getAccessUrlFromPublicReadBucket(String key) {
TransferManager transferManager = createTransferManage();
// 源桶地址
Region srcBucketRegion = new Region(privateOssConfig.getBucketRegion());
// 源桶名称
String srcBucketName = privateOssConfig.getBucketName();
// 目的桶名称
String destBucketName = ossConfig.getBucketName();
CopyObjectRequest copyObjectRequest = new CopyObjectRequest(srcBucketRegion, srcBucketName, key, destBucketName, key);
CopyResult copyResult;
try {
// 这里创建的 cosClient 是以复制的源端信息为基础的
Copy copy = transferManager.copy(copyObjectRequest, privateCosClient, null);
copyResult = copy.waitForCopyResult();
} catch (Exception e) {
log.info("object copy to public read bucket fail", e);
Thread.currentThread().interrupt();
throw new BusinessException("object copy to public read bucket fail");
}
// 记得用完关闭
transferManager.shutdownNow(false);
// 基于key生成公网可访问url
// ...
}
// privateCosClient 私读私写cos桶配置 以私读私写桶信息初始化
// cosClient 公读私写cos桶配置 以公读私写桶信息初始化
针对需求二
前端直接上传大文件业务服务二次加密处理后,获取公网可访问url
这种相当于在需求一的基础上进行扩展
直接对接COS上传文件后,会响应一个cosObjectKey
新增接口,获取私读私写桶文件临时访问链接,通过参数cosObjectKey
官方文档
这部分就不再提供代码样例了
这就是我对腾讯云COS桶的使用纪实,欢迎私信和我交流,一起探讨更优雅合理的方案。