前端如何進行單文件上傳云服務存儲
前端如何進行單文件上傳云服務存儲
http://zoo.zhengcaiyun.cn/blog/article/cloudservicestorage
在日常的開發過程中,我相信大家肯定會碰到很多的文件上傳需求,例如流程中的附件,設置頭像圖片等等內容,并且上傳的文件,為了前端頁面的加載性能,一般也都會選擇將文件上傳至云服務存儲當中去,之后直接使用文件的 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 文件的代碼示例
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) 等云服務數據,所以云服務廠家一般也會提供生成臨時令牌的方式,可以由后端服務生成一個自定義時效以及權限的訪問憑證提供給前端進行上傳,有效期到期后,這個訪問令牌就會失效,保證了前端上傳的安全性。
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步即可
- 生成 CanonicalRequest 字段
- 生成前面的 StringToSign
- 根據 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 格式時間
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('?? ~ 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;
}
}
}
常用云服務上傳格式 下面也提供了一些常用云服務上傳格式,上傳需要的最基礎格式,按照這個格式,組裝出需要的數據,然后發起上傳請求即可。下文示例中,如果使用 data 數據類型來進行校驗權限,上傳基本都是采用 form-data 數據封裝,上傳的 File 文件。而如果使用的是 headers 的類型進行數據校驗,上傳的 File 文件直接賦值請求中的 data 字段即可。
阿里云
{
"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
}
華為云
{
"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"
}
電信云 / 紫光云
{
"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 的方式進行處理,一方面,服務商敏感數據可以放在后端服務進行統一維護處理,另一方面,前端對于不同云服務上傳的配置數據進行統一的兼容處理,在發起后續的上傳,代碼邏輯也會比較的統一。
總結
本文僅針對了單文件上傳進行了梳理,對于多文件、分片上傳等還未涉及,后續還會繼續分享。不知道大家對于對接云服務上傳是否還有其他更好的處理方式,歡迎一起討論一下。
參考鏈接
阿里云 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)
華為云臨時授權 url 訪問(https://support.huaweicloud.com/sdk-browserjs-devg-obs/obs_24_0801.html)
電信云鑒權加密方式(https://www.ctyun.cn/document/10026693/10027129)
紫光云鑒權加密方式(https://www.unicloud.com/document/show-19262078.html)