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 上传资源的场景中经常会遇到以下场景导致上传失败等问题,对于用户体验较差:
因此 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 包括三种方式,分别是:
其中前两种方式对应的参数均是固定的,如果直接写在前端页面中会导致泄露的风险导致 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,否则将报错。
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,而是需要一段时间才可以捕获该异常,实际测试其延迟在网速正常的情况下为秒级或者分钟级别。
来源: https://yq.aliyun.com/articles/178451