关于阿里云 OSS 的介绍请参考官方文档:阿里云 OSS。
出于账号安全的考虑,前端使用 OSS 服务需要走临时授权,即拿一个临时凭证(STS Token)去调用 aliyun-oss SDK。关于临时授权请参考:RAM 和 STS 介绍,RAM 子账号,STS 临时授权访问 OSS。
以 NodeJs 为例,后端给前端颁发临时凭证的实现可参考:Node STS 授权访问
前端上传文件到阿里云的相关操作可参考:浏览器端上传文件
了解以上概念之后,接下来可以去阿里云 OSS 的控制台进行相关的设置了(前提是开通了 OSS 服务)。
首先,我们创建一个 bucket,一个存储文件的容器:
接着,我们需要给 bucket 设置跨域,这样我们才能在网页中调用 Aliyun OSS 服务器的接口:
接下来,前往 RAM 控制台进行子账号和权限的配置。
首先,我们创建一个用户,并给该用户分配调用 STS 服务 AssumeRole 接口的权限,这样待会儿后端就能以该用户的身份给前端分配 STS 凭证了:
我们需要保存一下该用户的 access key 和 access key secret,后端需要以此核实用户的身份。
该角色即有权限在前端调用 aliyun-oss SDK 上传文件的用户角色,例如我们创建一个只有上传权限的角色,命名为 uploader:
接下来我们需要给该角色分配权限,可以通过创建一条权限策略并分配给角色,该权限策略里面只包含了上传文件、分片上传相关的权限:
策略具体内容为:
{
"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 后台相关配置结束。接下来,我们来关注前后端的实现。
由于是前端负责上传,所以后端的任务比较简单,就是提供一个 STS Token 给前端。本文以 NodeJs 为例实现如下。
npm install ali-oss
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,方法接收的四个参数分别为:role arn, policy, expiration, session name。
Role arn 可以从 RAM 角色的详情页面获取:
Policy 是自定义的策略,由于已经为角色添加了权限策略,所以可以传null
。
Expiration 是 STS Token 的过期时间,应该在 15min ~ 60min 之间。当 Token 失效时前端需要重新获取。
Session name 为自定义的一个会话名称。
后端实现完成!
本文前端实现使用原生 JS,另外还有 ant design pro 的版本请参考 github 项目。
前端实现有几个关键点:
- 调用 aliyun-oss SDK 之前获取 STS Token
- 定义上传分片大小,如果文件小于分片大小则使用普通上传,否则使用分片上传
- 上传过程中能展示上传进度
- 上传过程中,如果 STS Token 快过期了,则先暂停上传重新获取 Token,接着进行断点续传
- 支持手动暂停、续传功能
- 上传完成后返回文件对应的下载地址
HTML 中包含文件选择器,上传、暂停、续传按钮,状态显示:
<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>
let credentials = null; // STS凭证
let ossClient = null; // oss客户端实例
const fileInput = document.getElementById('fileInput'); // 文件选择器
const status = document.getElementById('status'); // 状态显示元素
const bucket = 'mudontire-test'; // bucket名称
const region = 'oss-cn-shanghai'; // oss服务区域名称
const partSize = 1024 * 1024; // 每个分片大小(byte)
const parallel = 3; // 同时上传的分片数
const checkpoints = {}; // 所有分片上传文件的检查点
// 获取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);
});
}
// 创建OSS Client
async function initOSSClient() {
const { AccessKeyId, AccessKeySecret, SecurityToken } = credentials;
ossClient = new OSS({
accessKeyId: AccessKeyId,
accessKeySecret: AccessKeySecret,
stsToken: SecurityToken,
bucket,
region
});
}
async function upload() {
status.innerText = 'Uploading';
// 获取STS Token
await getCredential();
const { files } = fileInput;
const fileList = Array.from(files);
const uploadTasks = fileList.forEach(file => {
// 如果文件大学小于分片大小,使用普通上传,否则使用分片上传
if (file.size < partSize) {
commonUpload(file);
} else {
multipartUpload(file);
}
});
}
// 普通上传
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);
});
}
// 分片上传
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);
});
}
// 断点续传
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);
});
});
}
在 progress 回调中我们可以判断 STS Token 是否快过期了,如果快过期了则先取消上传获取新 Token 后在从之前的断点开始续传。
// 分片上传进度改变回调
async function onMultipartUploadProgress(progress, checkpoint) {
console.log(`${checkpoint.file.name} 上传进度 ${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();
}
}
// 暂停上传
function stop() {
status.innerText = 'Stopping';
if (ossClient) ossClient.cancel();
}
// 续传
function resume() {
status.innerText = 'Resuming';
if (ossClient) resumeMultipartUpload();
}
项目地址:https://github.com/MudOnTire/aliyun-oss-upload-demo。如果对大家有帮助,star一下吧。