在日常的开发过程中,我相信大家肯定会碰到很多的文件上传需求,例如流程中的附件,设置头像图片等等内容,并且上传的文件,为了前端页面的加载性能,一般也都会选择将文件上传至云服务存储当中去,之后直接使用文件的 cdn 路径来访问。那么问题来了,对于文件如何上传到云服务存储当中去大家是否了解呢?上传流程有遇到什么困难吗,所以这篇文章也借着我们团队遇到的一些问题,跟大家交流一下云服务文件存储当中的一些问题与解决方式。
不知道大家日常使用的上传方式是否和我们团队一致,之前上传文件方案中,我司后端团队会提供一个后端上传服务接口,前端直接使用这个接口进行文件上传,后端接受到完整文件后,会再通过调用云文件服务提供的后端 JAVA SDK 进行文件上传
这个方案的优缺点
优点:前端所有使用的上传接口统一,前端统一对接公司内部的上传服务,后端上传服务再去对接各个不同的云存储服务厂家,保证文件上传
缺点:后端服务需要接受所有的文件上传的流量,然后再次进行上传,服务器压力比较大。
基于上面提到的缺点,在经历过服务器压力过大,导致几次大文件上传失败、各种外地网络延迟导致超时故障之后,痛定思痛,决定要重新调整上传的方式。
既然后端服务上传需要走流程传输导致资源压力过大,那是否可以可以将压力转移到用户侧,使用用户的浏览器直连云存储服务进行上传呢?答案是当然可以,不然也就没有本文了。
在翻阅了几个不同的云服务的上传文档后发现,目前主流常用的前端上传方案会分为两种方式:
优点:前端不需要获取云服务的 AK (AccessKey ID),SK (AccessKey Secret) 信息,统一由后端接口提供对应上传所需的请求地址,数据格式即可,前端通过一个接口获取这些信息后,调用上传即可
缺点:各家云服务上传所需的数据格式都不相同,前端需要调研,解析这个数据格式
下面以大家常用的阿里云举例
webpack打包类型项目,可以先通过 npm install ali-oss 安装 SDK,以下为上传数据到 examplebucket 中 exampledir 目录下的exampleobject.txt 文件的代码示例
const OSS = require('ali-oss');
const client = new OSS({
// 以下为初始化参数
region: 'yourRegion',
// 从 STS 服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
accessKeyId: 'yourAccessKeyId',
accessKeySecret: 'yourAccessKeySecret',
// 从STS服务获取的安全令牌(SecurityToken)。
stsToken: 'yourSecurityToken',
// 填写 Bucket 名称(可以简单理解为,你上传不同文件到不同的文件夹命名)。
bucket: 'examplebucket'
});
// 从输入框获取 file 对象,例如 <input type="file" id="file" />。
let data;
// 创建并填写 Blob 数据。
//const data = new Blob(['Hello OSS']);
// 创建并填写 OSS Buffer内容。
//const data = new OSS.Buffer(['Hello OSS']);
const upload = document.getElementById("upload");
const headers = {
// 以下为上传时可以设置的一些 header 数据,不同云服务需要的不同,具体参考各个版本文档
// 'Content-Type': 'text/html', // 指定上传文件的类型。
// 'Cache-Control': 'no-cache', // 指定该 Object 被下载时网页的缓存行为。
// 'Content-Disposition': 'oss_download.txt', // 指定该 Object 被下载时的名称。
// 'Content-Encoding': 'UTF-8', // 指定该 Object 被下载时的内容编码格式。
// 'Expires': 'Wed, 08 Jul 2022 16:57:01 GMT', // 指定过期时间。
// 'x-oss-storage-class': 'Standard', // 指定 Object 的存储类型。
// 'x-oss-object-acl': 'private', // 指定 Object 的访问权限。
};
async function putObject(data) {
try {
// 填写Object完整路径。Object 完整路径中不能包含 Bucket 名称。
// 您可以通过自定义文件名(例如 exampleobject.txt )或文件完整路径(例如 exampledir/exampleobject.txt )的形式实现将数据上传到当前 Bucket 或 Bucket 中的指定目录。
// data 对象可以自定义为 file 对象、Blob 数据或者 OSS Buffer。
const result = awAIt client.put(
"exampledir/exampleobject.txt",
data
//{headers}
);
console.log(result);
} catch (e) {
console.log(e);
}
}
upload.addEventListener("click", () => {
data = document.getElementById("file").files[0];
putObject(data);
});
直接调用 SDK 中提供的 put 等方法即可完成文件上传
鉴于 SDK 上传方案中,会在代码中暴漏 AK (AccessKey ID),SK (AccessKey Secret) 等云服务数据,所以云服务厂家一般也会提供生成临时令牌的方式,可以由后端服务生成一个自定义时效以及权限的访问凭证提供给前端进行上传,有效期到期后,这个访问令牌就会失效,保证了前端上传的安全性。
1. 客户端向自己的后端应用发起请求,将文件类型,名称信息等传给后端,获取对应的上传信息以及授权签名信息 signature 等,
const UploadParams = {
"accessid":"LTAI5tBDFVar1hoq****",
"host":"http://post-test.oss-cn-hangzhou.aliyuncs.com",
"policy":"eyJleHBpcmF0aW9uIjoiMjAxNS0xMS0wNVQyMDoyMzoyM1oiLCJjxb25kaXRpb25zIjpbWyJjcb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MDAwXSxbInN0YXJ0cy13aXRoIiwiJGtleSIsInVzZXItZGlyXC8i****",
"signature":"VsxOcOudx******z93CLaXPz+4s=",
"expire":1446727949,
"dir":"user-dirs/"
}
2. 在获取到服务器返回的签名信息等内容后,客户端则可以通过 POST 或者 PUT 请求直接向云服务发送上传文件的请求(上传形式多种多样,并且有些云服务有要求上传数据类型为 form-data 格式)
// form-data 类型
let params = {
// key表示上传到 Bucket 内的 Object 的完整路径,例如 exampledir/exampleobject.txtObject,完整路径中不能包含 Bucket 名称。
// filename 表示待上传的本地文件名称。
'key' : key + '${filename}',
'policy': UploadParams.policy,
'OSSAccessKeyId': UploadParams.accessid,
// 设置服务端返回状态码为200,不设置则默认返回状态码204。
'success_action_status' : '200',
'signature': UploadParams.signature,
}
let requestData = new FormData();
Object.keys(params).map(key => {
requestData.Append(key, params[key]);
});
// 获取的上传 file 文件,file 必须为最后一个表单域,除 file 以外的其他表单域无顺序要求
requestData.append('file', fileObj);
// 非 form-data 类型(非阿里云云服务会遇到,以下代码仅举例,不代表真实使用场景)
let requestData = fileObj;
let headers = {
'key' : key + '${filename}',
'policy': UploadParams.policy,
'OSSAccessKeyId': UploadParams.accessid,
'success_action_status' : '200',
'signature': UploadParams.signature,
}
// 进行接口请求,上传文件
axIOS({
method: 'post',
url: params.host,
data: requestData,
headers: headers || {},
});
这里代码只是简单的示例,实际使用时需要对各个文件服务需要进行不同的适配。
对于获取 Signature 鉴权信息等内容时,后端服务在有文档或者 SDK 时,可以对接不同的云服务 JAVA SDK 直接进行生成临时授权的信息,在没有文档的情况下,则需要前端或者后端,针对各个不同的云服务,进行解析加密 Signature 的步骤(我司这里是前端进行了加密过程解析后,后续日常生成由后端服务完成)。
加密算法
此处我以紫光云的 Signature 生成步骤给大家简单介绍下加密算法的流程,不同的云服务,加密过程都比较类似。
图片来源:紫光云上传流程(https://www.unicloud.com/document/show-19262078.html)
以下是根据上述的加密流程写的测试生成 Signature 的代码部分,大家也可以自行测试试用。
按流程主要分成3步即可
const crypto = require('crypto');
const CryptoJS = require('crypto-js')
function zip() {
const filename = 'uploadTest.png'
// const date = new Date()
// const timeStampISO8601Format = `${date.toISOString().replace(/-/g, '').replace(/:/g, '').split('.')[0]}Z` // ISO 8601 格式
const timeStampISO8601Format = '20230101T000000Z' // ISO 8601 格式
const dateString = timeStampISO8601Format.substr(0, 8) // YYYYMMDD 格式时间
const uriFileName = uriEscapePath(filename)
const content = 'UNSIGNED-PAYLOAD'
// 生成 CanonicalRequest 字段
let CanonicalRequest = `PUTn${uriFileName}nncontent-disposition:attachment;filename=uploadTest.pngncontent-type:image/pngnhost:oos-cn.ctyunapi.cnnx-amz-content-sha256:${content}nx-amz-date:${timeStampISO8601Format}nncontent-disposition;content-type;host;x-amz-content-sha256;x-amz-daten${content}`
let hashedCanonicalRequest = crypto.createHash('sha256').update(CanonicalRequest).digest('hex');
// 生成前面的 StringToSign
const signStr = `AWS4-Hmac-SHA256n${timeStampISO8601Format}n${dateString}/cn/s3/aws4_requestn${hashedCanonicalRequest}`
//根据 AK (AccessKey ID),SK (AccessKey Secret) 生成 Signature
const AWSAccessKeyId = 'AWSAccessKeyId';
const AWSSecretAccessKey = 'AWSSecretAccessKey';
var DateKey = CryptoJS.HmacSHA256(dateString, `AWS4${AWSSecretAccessKey}`);
var DateRegionKey = CryptoJS.HmacSHA256('cn', DateKey);
var DateRegionServiceKey = CryptoJS.HmacSHA256('s3', DateRegionKey);
var SigningKey = CryptoJS.HmacSHA256('aws4_request', DateRegionServiceKey);
var Signature = CryptoJS.HmacSHA256(signStr, SigningKey);
console.log('