關(guān)于阿里云 OSS 的介紹請(qǐng)參考官方文檔:阿里云 OSS艺挪。
出于賬號(hào)安全的考慮不翩,前端使用 OSS 服務(wù)需要走臨時(shí)授權(quán),即拿一個(gè)臨時(shí)憑證(STS Token)去調(diào)用 aliyun-oss SDK麻裳。關(guān)于臨時(shí)授權(quán)請(qǐng)參考:RAM 和 STS 介紹口蝠,RAM 子賬號(hào),STS 臨時(shí)授權(quán)訪問 OSS津坑。
以 NodeJs 為例妙蔗,后端給前端頒發(fā)臨時(shí)憑證的實(shí)現(xiàn)可參考:Node STS 授權(quán)訪問
前端上傳文件到阿里云的相關(guān)操作可參考:瀏覽器端上傳文件
了解以上概念之后,接下來可以去阿里云 OSS 的控制臺(tái)進(jìn)行相關(guān)的設(shè)置了(前提是開通了 OSS 服務(wù))疆瑰。
阿里云 OSS 控制臺(tái)配置
1. 創(chuàng)建 Bucket
首先灭必,我們創(chuàng)建一個(gè) bucket,一個(gè)存儲(chǔ)文件的容器:
接著乃摹,我們需要給 bucket 設(shè)置跨域禁漓,這樣我們才能在網(wǎng)頁中調(diào)用 Aliyun OSS 服務(wù)器的接口:
2. 創(chuàng)建 RAM 用戶
接下來,前往 RAM 控制臺(tái)進(jìn)行子賬號(hào)和權(quán)限的配置孵睬。
首先播歼,我們創(chuàng)建一個(gè)用戶,并給該用戶分配調(diào)用 STS 服務(wù) AssumeRole 接口的權(quán)限,這樣待會(huì)兒后端就能以該用戶的身份給前端分配 STS 憑證了:
我們需要保存一下該用戶的 access key 和 access key secret秘狞,后端需要以此核實(shí)用戶的身份叭莫。
3. 創(chuàng)建 RAM 角色
該角色即有權(quán)限在前端調(diào)用 aliyun-oss SDK 上傳文件的用戶角色,例如我們創(chuàng)建一個(gè)只有上傳權(quán)限的角色烁试,命名為 uploader:
接下來我們需要給該角色分配權(quán)限雇初,可以通過創(chuàng)建一條權(quán)限策略并分配給角色,該權(quán)限策略里面只包含了上傳文件减响、分片上傳相關(guān)的權(quán)限:
策略具體內(nèi)容為:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"oss:PutObject",
"oss:InitiateMultipartUpload",
"oss:UploadPart",
"oss:UploadPartCopy",
"oss:CompleteMultipartUpload",
"oss:AbortMultipartUpload",
"oss:ListMultipartUploads",
"oss:ListParts"
],
"Resource": [
"acs:oss:*:*:mudontire-test",
"acs:oss:*:*:mudontire-test/*"
]
}
]
}
然后靖诗,把該策略賦予 uploader 角色:
到此,阿里云 OSS 后臺(tái)相關(guān)配置結(jié)束支示。接下來刊橘,我們來關(guān)注前后端的實(shí)現(xiàn)。
后端實(shí)現(xiàn)
由于是前端負(fù)責(zé)上傳颂鸿,所以后端的任務(wù)比較簡單促绵,就是提供一個(gè) STS Token 給前端。本文以 NodeJs 為例實(shí)現(xiàn)如下嘴纺。
1. 安裝 aliyun-oss SDK
npm install ali-oss
2. 生成 STS Token 并返回
const OSS = require('ali-oss');
const STS = OSS.STS;
const sts = new STS({
accessKeyId: process.env.ALIYUN_OSS_RULE_ASSUMER_ACCESS_KEY,
accessKeySecret: process.env.ALIYUN_OSS_RULE_ASSUMER_ACCESS_KEY_SECRET
});
async function getCredential(req, res, next) {
try {
const { credentials } = await sts.assumeRole(
'acs:ram::1582938330607257:role/uploader', // role arn
null, // policy
15 * 60, // expiration
'web-client' // session name
);
req.result = credentials;
next();
} catch (err) {
next(err);
}
}
其中败晴,access key 和 access key secret 保存在.env文件中。sts.assumeRole()返回的即為 STS Token栽渴,方法接收的四個(gè)參數(shù)分別為:role arn, policy, expiration, session name尖坤。
Role arn 可以從 RAM 角色的詳情頁面獲取:
Policy 是自定義的策略熔萧,由于已經(jīng)為角色添加了權(quán)限策略糖驴,所以可以傳null。
Expiration 是 STS Token 的過期時(shí)間佛致,應(yīng)該在 15min ~ 60min 之間贮缕。當(dāng) Token 失效時(shí)前端需要重新獲取。
Session name 為自定義的一個(gè)會(huì)話名稱俺榆。
后端實(shí)現(xiàn)完成感昼!
前端實(shí)現(xiàn)
本文前端實(shí)現(xiàn)使用原生 JS,另外還有 ant design pro 的版本請(qǐng)參考 github 項(xiàng)目罐脊。
前端實(shí)現(xiàn)有幾個(gè)關(guān)鍵點(diǎn):
調(diào)用 aliyun-oss SDK 之前獲取 STS Token
定義上傳分片大小定嗓,如果文件小于分片大小則使用普通上傳,否則使用分片上傳
上傳過程中能展示上傳進(jìn)度
上傳過程中萍桌,如果 STS Token 快過期了宵溅,則先暫停上傳重新獲取 Token,接著進(jìn)行斷點(diǎn)續(xù)傳
支持手動(dòng)暫停上炎、續(xù)傳功能
上傳完成后返回文件對(duì)應(yīng)的下載地址
1. 引入 aliyun-oss SDK
參考 引入 aliyun-oss SDK
2. HTML
HTML 中包含文件選擇器恃逻,上傳、暫停、續(xù)傳按鈕寇损,狀態(tài)顯示:
<div>
<input type="file" id='fileInput' multiple='true'>
<button id="uploadBtn" onclick="upload()">Upload</button>
<button id="stopBtn" onclick="stop()">Stop</button>
<button id="resumeBtn" onclick="resume()">resume</button>
<h2 id='status'></h2>
</div>
3. 定義變量
let credentials = null; // STS憑證
let ossClient = null; // oss客戶端實(shí)例
const fileInput = document.getElementById('fileInput'); // 文件選擇器
const status = document.getElementById('status'); // 狀態(tài)顯示元素
const bucket = 'mudontire-test'; // bucket名稱
const region = 'oss-cn-shanghai'; // oss服務(wù)區(qū)域名稱
const partSize = 1024 * 1024; // 每個(gè)分片大小(byte)
const parallel = 3; // 同時(shí)上傳的分片數(shù)
const checkpoints = {}; // 所有分片上傳文件的檢查點(diǎn)
4. 獲取 STS 憑證凸郑,創(chuàng)建 OSS Client
// 獲取STS Token
function getCredential() {
return fetch('http://localhost:5050/api/upload/credential')
.then(res => {
return res.json()
})
.then(res => {
credentials = res.result;
})
.catch(err => {
console.error(err);
});
}
// 創(chuàng)建OSS Client
async function initOSSClient() {
const { AccessKeyId, AccessKeySecret, SecurityToken } = credentials;
ossClient = new OSS({
accessKeyId: AccessKeyId,
accessKeySecret: AccessKeySecret,
stsToken: SecurityToken,
bucket,
region
});
}
5. 點(diǎn)擊上傳按鈕事件
async function upload() {
status.innerText = 'Uploading';
// 獲取STS Token
await getCredential();
const { files } = fileInput;
const fileList = Array.from(files);
const uploadTasks = fileList.forEach(file => {
// 如果文件大學(xué)小于分片大小,使用普通上傳矛市,否則使用分片上傳
if (file.size < partSize) {
commonUpload(file);
} else {
multipartUpload(file);
}
});
}
6. 普通上傳
// 普通上傳
async function commonUpload(file) {
if (!ossClient) {
await initOSSClient();
}
const fileName = file.name;
return ossClient.put(fileName, file).then(result => {
console.log(`Common upload ${file.name} succeeded, result === `, result)
}).catch(err => {
console.log(`Common upload ${file.name} failed === `, err);
});
}
7. 分片上傳
// 分片上傳
async function multipartUpload(file) {
if (!ossClient) {
await initOSSClient();
}
const fileName = file.name;
return ossClient.multipartUpload(fileName, file, {
parallel,
partSize,
progress: onMultipartUploadProgress
}).then(result => {
// 生成文件下載地址
const url = `http://${bucket}.${region}.aliyuncs.com/${fileName}`;
console.log(`Multipart upload ${file.name} succeeded, url === `, url)
}).catch(err => {
console.log(`Multipart upload ${file.name} failed === `, err);
});
}
8. 斷點(diǎn)續(xù)傳
// 斷點(diǎn)續(xù)傳
async function resumeMultipartUpload() {
Object.values(checkpoints).forEach((checkpoint) => {
const { uploadId, file, name } = checkpoint;
ossClient.multipartUpload(uploadId, file, {
parallel,
partSize,
progress: onMultipartUploadProgress,
checkpoint
}).then(result => {
console.log('before delete checkpoints === ', checkpoints);
delete checkpoints[checkpoint.uploadId];
console.log('after delete checkpoints === ', checkpoints);
const url = `http://${bucket}.${region}.aliyuncs.com/${name}`;
console.log(`Resume multipart upload ${file.name} succeeded, url === `, url)
}).catch(err => {
console.log('Resume multipart upload failed === ', err);
});
});
}
9. 分片上傳進(jìn)度
在 progress 回調(diào)中我們可以判斷 STS Token 是否快過期了芙沥,如果快過期了則先取消上傳獲取新 Token 后在從之前的斷點(diǎn)開始續(xù)傳。
// 分片上傳進(jìn)度改變回調(diào)
async function onMultipartUploadProgress(progress, checkpoint) {
console.log(`${checkpoint.file.name} 上傳進(jìn)度 ${progress}`);
checkpoints[checkpoint.uploadId] = checkpoint;
// 判斷STS Token是否將要過期浊吏,過期則重新獲取
const { Expiration } = credentials;
const timegap = 1;
if (Expiration && moment(Expiration).subtract(timegap, 'minute').isBefore(moment())) {
console.log(`STS token will expire in ${timegap} minutes而昨,uploading will pause and resume after getting new STS token`);
if (ossClient) {
ossClient.cancel();
}
await getCredential();
await resumeMultipartUpload();
}
}
10. 暫停、續(xù)傳按鈕點(diǎn)擊事件
// 暫停上傳
function stop() {
status.innerText = 'Stopping';
if (ossClient) ossClient.cancel();
}
// 續(xù)傳
function resume() {
status.innerText = 'Resuming';
if (ossClient) resumeMultipartUpload();
}
項(xiàng)目地址:https://github.com/MudOnTire/aliyun-oss-upload-demo