在日常的开发过程中,我相信大家肯定会碰到很多的文件上传需求,例如流程中的附件,设置头像图片等等内容,并且上传的文件,为了前端页面的加载性能,一般也都会选择将文件上传至云服务存储当中去,之后直接使用文件的 cdn 路径来访问。那么问题来了,对于文件如何上传到云服务存储当中去大家是否了解呢?上传流程有遇到什么困难吗,所以这篇文章也借着我们团队遇到的一些问题,跟大家交流一下云服务文件存储当中的一些问题与解决方式。
目前常用的上传方式
后端上传
不知道大家日常使用的上传方式是否和我们团队一致,之前上传文件方案中,我司后端团队会提供一个后端上传服务接口,前端直接使用这个接口进行文件上传,后端接受到完整文件后,会再通过调用云文件服务提供的后端 Java SDK 进行文件上传
这个方案的优缺点
优点:前端所有使用的上传接口统一,前端统一对接公司内部的上传服务,后端上传服务再去对接各个不同的云存储服务厂家,保证文件上传
缺点:后端服务需要接受所有的文件上传的流量,然后再次进行上传,服务器压力比较大。
基于上面提到的缺点,在经历过服务器压力过大,导致几次大文件上传失败、各种外地网络延迟导致超时故障之后,痛定思痛,决定要重新调整上传的方式。
前端上传
既然后端服务上传需要走流程传输导致资源压力过大,那是否可以可以将压力转移到用户侧,使用用户的浏览器直连云存储服务进行上传呢?答案是当然可以,不然也就没有本文了。
在翻阅了几个不同的云服务的上传文档后发现,目前主流常用的前端上传方案会分为两种方式:
- 前端调用各大云服务的 JavaScript SDK 进行上传
- 优点:无需后端服务介入,直接调用各个云服务 SDK 方法使用即可
- 缺点:前端需要获取各个云服务的 AK (AccessKey ID),SK (AccessKey Secret) 等账号信息,并且会暴漏在代码中,并且各个云服务场景会有对应的 SDK 以及调用方式,全部做了集成的话,包的体积可能不可控,并且有些云服务商,没有提供前端使用的SDK。
- 云服务会提供临时授权的 URL,前端可以直接通过这个授权 URL 访问云服务,进行文件上传
- 优点:前端不需要获取云服务的 AK (AccessKey ID),SK (AccessKey Secret) 信息,统一由后端接口提供对应上传所需的请求地址,数据格式即可,前端通过一个接口获取这些信息后,调用上传即可
- 缺点:各家云服务上传所需的数据格式都不相同,前端需要调研,解析这个数据格式
上传示例
下面以大家常用的阿里云举例
SDK上传
webpack打包类型项目,可以先通过 npm install ali-oss 安装 SDK,以下为上传数据到 examplebucket 中 exampledir 目录下的exampleobject.txt 文件的代码示例
typescript
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 等方法即可完成文件上传
临时 URL 上传(STS 临时授权)
鉴于 SDK 上传方案中,会在代码中暴漏 AK (AccessKey ID),SK (AccessKey Secret) 等云服务数据,所以云服务厂家一般也会提供生成临时令牌的方式,可以由后端服务生成一个自定义时效以及权限的访问凭证提供给前端进行上传,有效期到期后,这个访问令牌就会失效,保证了前端上传的安全性。
- 客户端向自己的后端应用发起请求,将文件类型,名称信息等传给后端,获取对应的上传信息以及授权签名信息 signature 等,
typescript
const UploadParams = { "accessid":"LTAI5tBDFVar1hoq****", "host":"http://post-test.oss-cn-hangzhou.aliyuncs.com", "policy":"eyJleHBpcmF0aW9uIjoiMjAxNS0xMS0wNVQyMDoyMzoyM1oiLCJjxb25kaXRpb25zIjpbWyJjcb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MDAwXSxbInN0YXJ0cy13aXRoIiwiJGtleSIsInVzZXItZGlyXC8i****", "signature":"VsxOcOudx******z93CLaXPz+4s=", "expire":1446727949, "dir":"user-dirs/" }
- 在获取到服务器返回的签名信息等内容后,客户端则可以通过 POST 或者 PUT 请求直接向云服务发送上传文件的请求(上传形式多种多样,并且有些云服务有要求上传数据类型为 form-data 格式)
typescript
// 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 生成步骤给大家简单介绍下加密算法的流程,不同的云服务,加密过程都比较类似。
以下是根据上述的加密流程写的测试生成 Signature 的代码部分,大家也可以自行测试试用。
按流程主要分成3步即可
- 生成 CanonicalRequest 字段
- 生成前面的 StringToSign
- 根据 AK (AccessKey ID),SK (AccessKey Secret) 生成 Signature,最后组装 Authorization。
javascript
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 = `PUT\n${uriFileName}\n\ncontent-disposition:attachment;filename=uploadTest.png\ncontent-type:image/png\nhost:oos-cn.ctyunapi.cn\nx-amz-content-sha256:${content}\nx-amz-date:${timeStampISO8601Format}\n\ncontent-disposition;content-type;host;x-amz-content-sha256;x-amz-date\n${content}` let hashedCanonicalRequest = crypto.createHash('sha256').update(CanonicalRequest).digest('hex'); // 生成前面的 StringToSign const signStr = `AWS4-HMAC-SHA256\n${timeStampISO8601Format}\n${dateString}/cn/s3/aws4_request\n${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('🚀 ~ Signature==', `${Signature}`); // 最后上传需要的 Authorization 数据 let Authorization = `AWS4-HMAC-SHA256 Credential=${AWSAccessKeyId}/${dateString}/cn/s3/aws4_request, SignedHeaders=content-disposition;content-type;host;x-amz-content-sha256;x-amz-date, Signature=${Signature}` console.log('🚀 ~ Authorization==', Authorization) } try { zip() } catch (error) { console.log('🚀 ~ error', error) } // uriEncode 方法 function uriEscapePath(string) { var parts = []; arrayEach(string.split("/"), function (part) { parts.push(uriEscape(part)); }); return parts.join("/"); } function uriEscape(string) { var output = encodeURIComponent(string); output = output.replace(/[^A-Za-z0-9_.~\-%]+/g, escape); output = output.replace(/[*]/g, function (ch) { return "%" + ch.charCodeAt(0).toString(16).toUpperCase(); }); return output; } function arrayEach(array, iterFunction) { for (var idx in array) { if (Object.prototype.hasOwnProperty.call(array, idx)) { var ret = iterFunction.call(this, array[idx], parseInt(idx, 10)); if (ret === {}) break; } } }
常用云服务上传格式 下面也提供了一些常用云服务上传格式,上传需要的最基础格式,按照这个格式,组装出需要的数据,然后发起上传请求即可。下文示例中,如果使用 data 数据类型来进行校验权限,上传基本都是采用 form-data 数据封装,上传的 File 文件。而如果使用的是 headers 的类型进行数据校验,上传的 File 文件直接赋值请求中的 data 字段即可。
阿里云
json
{ "method":"POST", // 上传的请求类型 "dataType":"formData", // 为了区分上传数据的 form-data 类型,可自己任意定义 "data":{ // "OSSAccessKeyId":"accessKeyId", "signature":"计算后签名Signature", "success_action_status":"200", "Content-Disposition":"attachment;filename=encodeURI(filename)", "key":"上传文件路径/上传的文件fileId", "policy":"后端返回的policy", "file": File, // 上传的 file 文件 }, "action":"上传服务的域名" // 前端发起上传的请求 URL }
华为云
json
{ "headers":{ "X-Requested-With":null, "Content-Disposition":"attachment;filename=encodeURI(filename)", "Content-Type":"文件类型" }, "method":"PUT", "data": File, // 上传的 file 文件 "dataType":"text",// 为了区分上传数据的 form-data 类型,可自己任意定义 "action":"https://上传服务url域名/bucket/${fileId}?AccessKeyId=${AccessKeyId}&Expires=${过期时间}&Signature=${计算后签名Signature}", "fileId":"文件名称,可以使用唯一id" }
电信云 / 紫光云
json
{ "headers":{ "Authorization":"AWS4-HMAC-SHA256 Credential=<your-access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request , SignedHeaders=content-disposition;content-type;host;x-amz-content-sha256;x-amz-date, Signature=${计算后前面Signature}", "x-amz-content-sha256":"UNSIGNED-PAYLOAD", "x-amz-date":"20230202T093208Z(服务器时间)", "Content-Disposition":"attachment;filename=encodeURI(fileName)", "Content-Type":"文件类型" }, "method":"PUT", "data": File, // 上传的 file 文件 "dataType":"text", "action":"https://上传服务url域名/bucket/${fileId}" }
从这几种云服务的类型可以看出,上传参数区分,基本分为了data 数据校验上传或者 headers 校验上传,上面的文件上传实例代码基本可以包括目前的几种上传请求方式
上传推荐
以上两种方式都可以满足前端直连上传的需求,大家选择的时候可以根据自己的实际场景进行选择即可。
当你的上传云服务比较单一,无论是 SDK 上传,或者临时授权 URL 上传都可以选择,不过如果对账号安全比较敏感,第一种方式也可以选择加密或者配置数据的方式进行账号的传递。
而鉴于我司有多种云服务上传的需求,并且 SDK 上传方式需要暴漏 AK (AccessKey ID),SK (AccessKey Secret) 等业务数据, SDK 的集成也会使后续输出的 NPM 包依赖内容过大,还需要兼容不同 SDK不同的上传调用方法,所以我司最后是选择了临时授权 URL 的方式进行处理,一方面,服务商敏感数据可以放在后端服务进行统一维护处理,另一方面,前端对于不同云服务上传的配置数据进行统一的兼容处理,在发起后续的上传,代码逻辑也会比较的统一。
总结
本文仅针对了单文件上传进行了梳理,对于多文件、分片上传等还未涉及,后续还会继续分享。不知道大家对于对接云服务上传是否还有其他更好的处理方式,欢迎一起讨论一下。