前端如何進(jìn)行單文件上傳云服務(wù)存儲(chǔ)
前端如何進(jìn)行單文件上傳云服務(wù)存儲(chǔ)
http://zoo.zhengcaiyun.cn/blog/article/cloudservicestorage
在日常的開發(fā)過程中,我相信大家肯定會(huì)碰到很多的文件上傳需求,例如流程中的附件,設(shè)置頭像圖片等等內(nèi)容,并且上傳的文件,為了前端頁面的加載性能,一般也都會(huì)選擇將文件上傳至云服務(wù)存儲(chǔ)當(dāng)中去,之后直接使用文件的 cdn 路徑來訪問。那么問題來了,對(duì)于文件如何上傳到云服務(wù)存儲(chǔ)當(dāng)中去大家是否了解呢?上傳流程有遇到什么困難嗎,所以這篇文章也借著我們團(tuán)隊(duì)遇到的一些問題,跟大家交流一下云服務(wù)文件存儲(chǔ)當(dāng)中的一些問題與解決方式。
目前常用的上傳方式
后端上傳
不知道大家日常使用的上傳方式是否和我們團(tuán)隊(duì)一致,之前上傳文件方案中,我司后端團(tuán)隊(duì)會(huì)提供一個(gè)后端上傳服務(wù)接口,前端直接使用這個(gè)接口進(jìn)行文件上傳,后端接受到完整文件后,會(huì)再通過調(diào)用云文件服務(wù)提供的后端 Java SDK 進(jìn)行文件上傳
這個(gè)方案的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):前端所有使用的上傳接口統(tǒng)一,前端統(tǒng)一對(duì)接公司內(nèi)部的上傳服務(wù),后端上傳服務(wù)再去對(duì)接各個(gè)不同的云存儲(chǔ)服務(wù)廠家,保證文件上傳
缺點(diǎn):后端服務(wù)需要接受所有的文件上傳的流量,然后再次進(jìn)行上傳,服務(wù)器壓力比較大。
基于上面提到的缺點(diǎn),在經(jīng)歷過服務(wù)器壓力過大,導(dǎo)致幾次大文件上傳失敗、各種外地網(wǎng)絡(luò)延遲導(dǎo)致超時(shí)故障之后,痛定思痛,決定要重新調(diào)整上傳的方式。
前端上傳
既然后端服務(wù)上傳需要走流程傳輸導(dǎo)致資源壓力過大,那是否可以可以將壓力轉(zhuǎn)移到用戶側(cè),使用用戶的瀏覽器直連云存儲(chǔ)服務(wù)進(jìn)行上傳呢?答案是當(dāng)然可以,不然也就沒有本文了。
在翻閱了幾個(gè)不同的云服務(wù)的上傳文檔后發(fā)現(xiàn),目前主流常用的前端上傳方案會(huì)分為兩種方式:
- 前端調(diào)用各大云服務(wù)的 JavaScript SDK 進(jìn)行上傳
- 優(yōu)點(diǎn):無需后端服務(wù)介入,直接調(diào)用各個(gè)云服務(wù) SDK 方法使用即可
- 缺點(diǎn):前端需要獲取各個(gè)云服務(wù)的 AK (AccessKey ID),SK (AccessKey Secret) 等賬號(hào)信息,并且會(huì)暴漏在代碼中,并且各個(gè)云服務(wù)場(chǎng)景會(huì)有對(duì)應(yīng)的 SDK 以及調(diào)用方式,全部做了集成的話,包的體積可能不可控,并且有些云服務(wù)商,沒有提供前端使用的SDK。
- 云服務(wù)會(huì)提供臨時(shí)授權(quán)的 URL,前端可以直接通過這個(gè)授權(quán) URL 訪問云服務(wù),進(jìn)行文件上傳
優(yōu)點(diǎn):前端不需要獲取云服務(wù)的 AK (AccessKey ID),SK (AccessKey Secret) 信息,統(tǒng)一由后端接口提供對(duì)應(yīng)上傳所需的請(qǐng)求地址,數(shù)據(jù)格式即可,前端通過一個(gè)接口獲取這些信息后,調(diào)用上傳即可
缺點(diǎn):各家云服務(wù)上傳所需的數(shù)據(jù)格式都不相同,前端需要調(diào)研,解析這個(gè)數(shù)據(jù)格式
上傳示例
下面以大家常用的阿里云舉例
SDK上傳
webpack打包類型項(xiàng)目,可以先通過 npm install ali-oss 安裝 SDK,以下為上傳數(shù)據(jù)到 examplebucket 中 exampledir 目錄下的exampleobject.txt 文件的代碼示例
const OSS = require('ali-oss');
const client = new OSS({
// 以下為初始化參數(shù)
region: 'yourRegion',
// 從 STS 服務(wù)獲取的臨時(shí)訪問密鑰(AccessKey ID和AccessKey Secret)。
accessKeyId: 'yourAccessKeyId',
accessKeySecret: 'yourAccessKeySecret',
// 從STS服務(wù)獲取的安全令牌(SecurityToken)。
stsToken: 'yourSecurityToken',
// 填寫 Bucket 名稱(可以簡(jiǎn)單理解為,你上傳不同文件到不同的文件夾命名)。
bucket: 'examplebucket'
});
// 從輸入框獲取 file 對(duì)象,例如 <input type="file" id="file" />。
let data;
// 創(chuàng)建并填寫 Blob 數(shù)據(jù)。
//const data = new Blob(['Hello OSS']);
// 創(chuàng)建并填寫 OSS Buffer內(nèi)容。
//const data = new OSS.Buffer(['Hello OSS']);
const upload = document.getElementById("upload");
const headers = {
// 以下為上傳時(shí)可以設(shè)置的一些 header 數(shù)據(jù),不同云服務(wù)需要的不同,具體參考各個(gè)版本文檔
// 'Content-Type': 'text/html', // 指定上傳文件的類型。
// 'Cache-Control': 'no-cache', // 指定該 Object 被下載時(shí)網(wǎng)頁的緩存行為。
// 'Content-Disposition': 'oss_download.txt', // 指定該 Object 被下載時(shí)的名稱。
// 'Content-Encoding': 'UTF-8', // 指定該 Object 被下載時(shí)的內(nèi)容編碼格式。
// 'Expires': 'Wed, 08 Jul 2022 16:57:01 GMT', // 指定過期時(shí)間。
// 'x-oss-storage-class': 'Standard', // 指定 Object 的存儲(chǔ)類型。
// 'x-oss-object-acl': 'private', // 指定 Object 的訪問權(quán)限。
};
async function putObject(data) {
try {
// 填寫Object完整路徑。Object 完整路徑中不能包含 Bucket 名稱。
// 您可以通過自定義文件名(例如 exampleobject.txt )或文件完整路徑(例如 exampledir/exampleobject.txt )的形式實(shí)現(xiàn)將數(shù)據(jù)上傳到當(dāng)前 Bucket 或 Bucket 中的指定目錄。
// data 對(duì)象可以自定義為 file 對(duì)象、Blob 數(shù)據(jù)或者 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);
});
直接調(diào)用 SDK 中提供的 put 等方法即可完成文件上傳
臨時(shí) URL 上傳(STS 臨時(shí)授權(quán))
鑒于 SDK 上傳方案中,會(huì)在代碼中暴漏 AK (AccessKey ID),SK (AccessKey Secret) 等云服務(wù)數(shù)據(jù),所以云服務(wù)廠家一般也會(huì)提供生成臨時(shí)令牌的方式,可以由后端服務(wù)生成一個(gè)自定義時(shí)效以及權(quán)限的訪問憑證提供給前端進(jìn)行上傳,有效期到期后,這個(gè)訪問令牌就會(huì)失效,保證了前端上傳的安全性。
1. 客戶端向自己的后端應(yīng)用發(fā)起請(qǐng)求,將文件類型,名稱信息等傳給后端,獲取對(duì)應(yīng)的上傳信息以及授權(quán)簽名信息 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. 在獲取到服務(wù)器返回的簽名信息等內(nèi)容后,客戶端則可以通過 POST 或者 PUT 請(qǐng)求直接向云服務(wù)發(fā)送上傳文件的請(qǐng)求(上傳形式多種多樣,并且有些云服務(wù)有要求上傳數(shù)據(jù)類型為 form-data 格式)
// form-data 類型
let params = {
// key表示上傳到 Bucket 內(nèi)的 Object 的完整路徑,例如 exampledir/exampleobject.txtObject,完整路徑中不能包含 Bucket 名稱。
// filename 表示待上傳的本地文件名稱。
'key' : key + '${filename}',
'policy': UploadParams.policy,
'OSSAccessKeyId': UploadParams.accessid,
// 設(shè)置服務(wù)端返回狀態(tài)碼為200,不設(shè)置則默認(rèn)返回狀態(tài)碼204。
'success_action_status' : '200',
'signature': UploadParams.signature,
}
let requestData = new FormData();
Object.keys(params).map(key => {
requestData.append(key, params[key]);
});
// 獲取的上傳 file 文件,file 必須為最后一個(gè)表單域,除 file 以外的其他表單域無順序要求
requestData.append('file', fileObj);
// 非 form-data 類型(非阿里云云服務(wù)會(huì)遇到,以下代碼僅舉例,不代表真實(shí)使用場(chǎng)景)
let requestData = fileObj;
let headers = {
'key' : key + '${filename}',
'policy': UploadParams.policy,
'OSSAccessKeyId': UploadParams.accessid,
'success_action_status' : '200',
'signature': UploadParams.signature,
}
// 進(jìn)行接口請(qǐng)求,上傳文件
axios({
method: 'post',
url: params.host,
data: requestData,
headers: headers || {},
});
這里代碼只是簡(jiǎn)單的示例,實(shí)際使用時(shí)需要對(duì)各個(gè)文件服務(wù)需要進(jìn)行不同的適配。
加密算法和解析
對(duì)于獲取 Signature 鑒權(quán)信息等內(nèi)容時(shí),后端服務(wù)在有文檔或者 SDK 時(shí),可以對(duì)接不同的云服務(wù) JAVA SDK 直接進(jìn)行生成臨時(shí)授權(quán)的信息,在沒有文檔的情況下,則需要前端或者后端,針對(duì)各個(gè)不同的云服務(wù),進(jìn)行解析加密 Signature 的步驟(我司這里是前端進(jìn)行了加密過程解析后,后續(xù)日常生成由后端服務(wù)完成)。
加密算法
此處我以紫光云的 Signature 生成步驟給大家簡(jiǎn)單介紹下加密算法的流程,不同的云服務(wù),加密過程都比較類似。
圖片來源:紫光云上傳流程(https://www.unicloud.com/document/show-19262078.html)
以下是根據(jù)上述的加密流程寫的測(cè)試生成 Signature 的代碼部分,大家也可以自行測(cè)試試用。
按流程主要分成3步即可
- 生成 CanonicalRequest 字段
- 生成前面的 StringToSign
- 根據(jù) AK (AccessKey ID),SK (AccessKey Secret) 生成 Signature,最后組裝 Authorization。
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 格式時(shí)間
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}`
//根據(jù) 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 數(shù)據(jù)
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('?? ~ Authorizatinotallow==', 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;
}
}
}
常用云服務(wù)上傳格式 下面也提供了一些常用云服務(wù)上傳格式,上傳需要的最基礎(chǔ)格式,按照這個(gè)格式,組裝出需要的數(shù)據(jù),然后發(fā)起上傳請(qǐng)求即可。下文示例中,如果使用 data 數(shù)據(jù)類型來進(jìn)行校驗(yàn)權(quán)限,上傳基本都是采用 form-data 數(shù)據(jù)封裝,上傳的 File 文件。而如果使用的是 headers 的類型進(jìn)行數(shù)據(jù)校驗(yàn),上傳的 File 文件直接賦值請(qǐng)求中的 data 字段即可。
阿里云
{
"method":"POST", // 上傳的請(qǐng)求類型
"dataType":"formData", // 為了區(qū)分上傳數(shù)據(jù)的 form-data 類型,可自己任意定義
"data":{ //
"OSSAccessKeyId":"accessKeyId",
"signature":"計(jì)算后簽名Signature",
"success_action_status":"200",
"Content-Disposition":"attachment;filename=encodeURI(filename)",
"key":"上傳文件路徑/上傳的文件fileId",
"policy":"后端返回的policy",
"file": File, // 上傳的 file 文件
},
"action":"上傳服務(wù)的域名" // 前端發(fā)起上傳的請(qǐng)求 URL
}
華為云
{
"headers":{
"X-Requested-With":null,
"Content-Disposition":"attachment;filename=encodeURI(filename)",
"Content-Type":"文件類型"
},
"method":"PUT",
"data": File, // 上傳的 file 文件
"dataType":"text",// 為了區(qū)分上傳數(shù)據(jù)的 form-data 類型,可自己任意定義
"action":"https://上傳服務(wù)url域名/bucket/${fileId}?AccessKeyId=${AccessKeyId}&Expires=${過期時(shí)間}&Signature=${計(jì)算后簽名Signature}",
"fileId":"文件名稱,可以使用唯一id"
}
電信云 / 紫光云
{
"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=${計(jì)算后前面Signature}",
"x-amz-content-sha256":"UNSIGNED-PAYLOAD",
"x-amz-date":"20230202T093208Z(服務(wù)器時(shí)間)",
"Content-Disposition":"attachment;filename=encodeURI(fileName)",
"Content-Type":"文件類型"
},
"method":"PUT",
"data": File, // 上傳的 file 文件
"dataType":"text",
"action":"https://上傳服務(wù)url域名/bucket/${fileId}"
}
從這幾種云服務(wù)的類型可以看出,上傳參數(shù)區(qū)分,基本分為了data 數(shù)據(jù)校驗(yàn)上傳或者 headers 校驗(yàn)上傳,上面的文件上傳實(shí)例代碼基本可以包括目前的幾種上傳請(qǐng)求方式
上傳推薦
以上兩種方式都可以滿足前端直連上傳的需求,大家選擇的時(shí)候可以根據(jù)自己的實(shí)際場(chǎng)景進(jìn)行選擇即可。
當(dāng)你的上傳云服務(wù)比較單一,無論是 SDK 上傳,或者臨時(shí)授權(quán) URL 上傳都可以選擇,不過如果對(duì)賬號(hào)安全比較敏感,第一種方式也可以選擇加密或者配置數(shù)據(jù)的方式進(jìn)行賬號(hào)的傳遞。
而鑒于我司有多種云服務(wù)上傳的需求,并且 SDK 上傳方式需要暴漏 AK (AccessKey ID),SK (AccessKey Secret) 等業(yè)務(wù)數(shù)據(jù), SDK 的集成也會(huì)使后續(xù)輸出的 NPM 包依賴內(nèi)容過大,還需要兼容不同 SDK不同的上傳調(diào)用方法,所以我司最后是選擇了臨時(shí)授權(quán) URL 的方式進(jìn)行處理,一方面,服務(wù)商敏感數(shù)據(jù)可以放在后端服務(wù)進(jìn)行統(tǒng)一維護(hù)處理,另一方面,前端對(duì)于不同云服務(wù)上傳的配置數(shù)據(jù)進(jìn)行統(tǒng)一的兼容處理,在發(fā)起后續(xù)的上傳,代碼邏輯也會(huì)比較的統(tǒng)一。
總結(jié)
本文僅針對(duì)了單文件上傳進(jìn)行了梳理,對(duì)于多文件、分片上傳等還未涉及,后續(xù)還會(huì)繼續(xù)分享。不知道大家對(duì)于對(duì)接云服務(wù)上傳是否還有其他更好的處理方式,歡迎一起討論一下。
參考鏈接
阿里云 SDK 上傳(https://help.aliyun.com/document_detail/383950.html)
阿里云 PostObject 解析(https://help.aliyun.com/document_detail/31988.htm?spm=a2c4g.11186623.0.0.160750c5esmHVG#section-d5z-1ww-wdb)
阿里云后端簽名后直傳(https://help.aliyun.com/document_detail/31926.html)
華為云臨時(shí)授權(quán) url 訪問(https://support.huaweicloud.com/sdk-browserjs-devg-obs/obs_24_0801.html)
電信云鑒權(quán)加密方式(https://www.ctyun.cn/document/10026693/10027129)
紫光云鑒權(quán)加密方式(https://www.unicloud.com/document/show-19262078.html)