OSS提供JS SDK方式可以让用户直接在前端页面中嵌入js包直接操作OSS,但是该方式操作OSS最容易出现的问题即是AccessKeyId和AccessKeySecret的泄露导致被恶意访问的情况。因此提供了STS的方式动态获取token操作OSS,随之而来带来了整个业务逻辑的复杂度。今天在这里给大家介绍STS方式进行断点续传的实践方法。
基本概念
1. OSS Javascript SDK
OSS提供了海量、安全、低成本、高可靠的云存储服务,因此该产品最基本的功能即是实现将客户端的资源上传至OSS。OSS底层提供了与平台无关的RESTful API接口,但是该接口需要用户自行实现拼接发到OSS服务器的请求包以及签名参数,对于客户的技术水平提出了较高的要求。
因此OSS提供了各种语言的SDK帮助用户较方便的接入OSS并实现与OSS服务器端的对接操作,详细的SDK列表请参考 OSS SDK 列表,其中绝大多数的SDK均是服务器端的SDK,是无法直接在前端页面中使用的。而Javascript SDK包括浏览器应用和node.js两种方式,浏览器应用即可以实现前端页面直接操作OSS,避免增加应用服务器的负载压力。
2. 断点续传功能
在OSS上传资源的场景中经常会遇到以下场景导致上传失败等问题,对于用户体验较差:
- 上传超过100MB大小的文件;
- 网络条件较差,和OSS的服务器之间的链接经常断开;
- 上传文件需要实时掌握上传进度;
- 业务逻辑需要断点续传。
因此OSS提供了断点续传的方法帮助用户改善该场景。断点续传的方法主要是通过checkpoint和分块上传的接口实现的断点续传。
分块上传是SDK将用户待上传的完整文件分成若干个分片后分别上传,然后上传完成后验证各个分块的etag保证数据正确性后进行合并完成的。因此分块上传的逻辑主要包括:
InitiateMultipartUpload(生成UploadId并设置Object的HTTP头)->UploadPart(上传分块,可并行上传)->CompleteMultipartUpload(根据part列表验证每个part的有效性后合并为完整的Object)。
断点续传即是在progress参数中将断点信息抛出记录在checkpoint变量中。后续续传时即将之前记录的checkpoint信息重新传入multipartUpload接口即可以实现,对应的demo请参考:
var co = require('co');
var OSS = require('ali-oss')
var client = new OSS({
region: '<Your region>',
accessKeyId: '<Your AccessKeyId>',
accessKeySecret: '<Your AccessKeySecret>',
bucket: 'Your bucket name'
});
co(function* () {
var checkpoint;
// retry 5 times
for (var i = 0; i < 5; i++) {
var result = yield client.multipartUpload('object-key', 'local-file', {
checkpoint: checkpoint,
progress: function* (percentage, cpt) {
checkpoint = cpt;
}
});
console.log(result);
break; // break if success
} catch (err) {
console.log(err);
}
}
}).catch(function (err) {
console.log(err);
});
注意:Javascript sdk对于上述的三个部分做了封装,因此对于用户来讲不需要分步操作,而仅需要统一调用multipartUpload接口即可实现。
3. STS的Token功能
OSS SDK均需要通过AccesssKeyId和AccessKeySecret授权后才可以访问OSS,而AccesssKeyId和AccessKeySecret包括三种方式,分别是:
- 主账号AccesssKeyId和AccessKeySecret;
- 子账号AccesssKeyId和AccessKeySecret;
- STS动态生成的AccesssKeyId和AccessKeySecret和AccessKeyToken。
其中前两种方式对应的参数均是固定的,如果直接写在前端页面中会导致泄露的风险导致OSS被恶意操作;而第三种STS动态生成的Token则具有时间戳,仅有在时间戳有效时间内才可以使用。能够提升用户操作安全性。
STS的token一般可以前端代码向应用服务器请求得到结果,也可以使用JS SDK直接操作,demo请参考:
var OSS = require('ali-oss');
var STS = OSS.STS;
var co = require('co');
var sts = new STS({
accessKeyId: '<子账号的AccessKeyId>',
accessKeySecret: '<子账号的AccessKeySecret>'
});
co(function* () {
var token = yield sts.assumeRole(
'<role-arn>', '<policy>', '<expiration>', '<session-name>');
var client = new OSS({
region: '<region>',
accessKeyId: token.credentials.AccessKeyId,
accessKeySecret: token.credentials.AccessKeySecret,
stsToken: token.credentials.SecurityToken,
bucket: '<bucket-name>'
});
}).catch(function (err) {
console.log(err);
});
注意:使用STS的token创建OSSClient对象必须同时提供accessKeyId、accessKeySecret和stsToken,否则将报错。
JS SDK使用STS方式实现断点续传
STS的token有时间戳,当超过时间戳会导致后续的请求将无法正常请求,在断点续传时就可能出现当token已经过期后仍然需要上传的操作。为解决该问题我们建议在multipartUpload抛出的yichan异常中获取该异常重新获取token进行续传。最佳实践的代码请参考:
<!DOCTYPE html>
<html>
<head>
<script src="http://gosspublic.alicdn.com/aliyun-oss-sdk-4.10.0.min.js"></script>
</head>
<body>
<input type="file" id="uploadFile" />
<script type="text/javascript">
stsAccessKeyId = ""
stsAccessKeySecret = ""
stsToken = ""
var checkpoint_temp;
function multipartUploadWithSts(storeAs, file, cpt) {
OSS.urllib.request("http://localhost/sts-server/sts.php", {
method: 'GET'}, function (err, response) {
if (err) {
return alert(err);
}
try {
result = JSON.parse(response);
} catch (e) {
errmsg = 'parse sts response info error: ' + e.message;
return alert(errmsg);
}
console.log(result)
client = new OSS.Wrapper({
accessKeyId: result.AccessKeyId,
accessKeySecret: result.AccessKeySecret,
stsToken: result.SecurityToken,
bucket: 'dongchics',
endpoint: 'http://oss-cn-hangzhou.aliyuncs.com'
});
multitest(client, storeAs, file, cpt);
})
};
var upload = function () {
var client = null;
var file = document.getElementById('uploadFile').files[0];
console.log(file);
var storeAs = file['name'];
console.log("upload file=",file)
multipartUploadWithSts(storeAs, file)
};
function multitest (ossClient, storeAs, file, cpt) {
//console.log(file.name + ' => ' + storeAs);
var checkpoint_temp;
if (cpt) {
console.log("multitest with cpt")
ossClient.multipartUpload(storeAs, file,{
parallel: 2,
checkpoint: cpt,
progress: function* (percent, cpt) {
console.log('Progress: ' + percent);
checkpoint_temp = cpt
}
}).then(function (result) {
console.log(result);
}).catch(function (err) {
console.log(err);
multipartUploadWithSts(storeAs, file, checkpoint_temp)
});
} else {
console.log("multitest without cpt")
ossClient.multipartUpload(storeAs, file,{
parallel: 2,
progress: function* (percent, cpt) {
console.log('Progress: ' + percent);
checkpoint_temp = cpt
}
}).then(function (result) {
console.log(result);
}).catch(function (err) {
console.log(err);
multipartUploadWithSts(storeAs, file, checkpoint_temp)
});
}
};
document.getElementById('uploadFile').onchange = upload;
</script>
</body>
</html>
这段demo主要包括一下几部分:
1.前端向服务器端发起请求sts的token的请求用户初始化OSSClient对象。而服务器端收到该请求后返回AccessKeyId、AccessKeySecret、SecurityToken和Expiration,这里使用的STS的php sdk实现的,代码可以参考:
<?php
include_once 'aliyun-php-sdk-core/Config.php';
use Sts\Request\V20150401 as Sts;
function read_file($fname)
{
$content = '';
if (!file_exists($fname)) {
echo "The file $fname does not exist\n";
exit (0);
}
$handle = fopen($fname, "rb");
while (!feof($handle)) {
$content .= fread($handle, 10000);
}
fclose($handle);
return $content;
}
$content = read_file('./config.json');
$myjsonarray = json_decode($content);
$accessKeyID = $myjsonarray->AccessKeyID;
$accessKeySecret = $myjsonarray->AccessKeySecret;
$roleArn = $myjsonarray->RoleArn;
$tokenExpire = $myjsonarray->TokenExpireTime;
$policy = read_file($myjsonarray->PolicyFile);
$iClientProfile = DefaultProfile::getProfile("cn-hangzhou", $accessKeyID, $accessKeySecret);
$client = new DefaultAcsClient($iClientProfile);
$request = new Sts\AssumeRoleRequest();
$request->setRoleSessionName("client_name");
$request->setRoleArn($roleArn);
$request->setPolicy($policy);
$request->setDurationSeconds($tokenExpire);
$response = $client->doAction($request);
$rows = array();
$body = $response->getBody();
$content = json_decode($body);
$rows['status'] = $response->getStatus();
if ($response->getStatus() == 200)
{
$rows['AccessKeyId'] = $content->Credentials->AccessKeyId;
$rows['AccessKeySecret'] = $content->Credentials->AccessKeySecret;
$rows['Expiration'] = $content->Credentials->Expiration;
$rows['SecurityToken'] = $content->Credentials->SecurityToken;
}
else
{
$rows['AccessKeyId'] = "";
$rows['AccessKeySecret'] = "";
$rows['Expiration'] = "";
$rows['SecurityToken'] = "";
}
echo json_encode($rows);
return;
?>
2.在multitest方法中实现断点续传,其中cpt用来判断是否已有断点信息,如果没有该信息则初始分块上传方法,并将断点信息存放在checkpoint_temp变量中;如果有该信息则会将checkpoint_temp作为参数传入multipartUpload方法。
3.在catch异常中重复调用断点续传方法以保证token过期后继续上传。
注意:JS SDK封装的multipartUpload接口实现了并行分块上传,因此当第一个由于token过期出现的403错误后并不会立刻catch到该err,而是需要一段时间才可以捕获该异常,实际测试其延迟在网速正常的情况下为秒级或者分钟级别。