Web端直传实践

场景概述

用户通过Web上传,是BOS使用需求中很典型的一种应用场景。在该场景下,用户通常采用应用服务器中转的模型进行文件上传。

  1. 用户先将文件通过Web上传到应用服务器;
  2. 应用服务器再将文件上传到BOS。

在该模型中,存在如下3个缺点:

  • 上传速度慢。因为需要经过应用服务器中转,与客户端数据直传到BOS相比,网络传送增加了一倍。
  • 扩展性差。随着用户数量的增加,应用服务器可能成为传输瓶颈。
  • 成本高。应用服务器的部署和维护需要一定成本,如客户端数据直传到BOS,将节省应用服务器的开销,且BOS上传的流量是免费的。

因此,在该场景下,我们推荐您使用bce-bos-uploader工具实现客户端直传BOS的方式。

bce bos uploader

Baidu Cloud Engine BOS Uploader(bce-bos-uploader)是百度智能云基于Javascript SDK开发的一个ui组件,为了方便用户开发web直传应用而专门提供的,使用该工具用很少的几行代码就可以完成跟BOS服务的对接。bce-bos-uploader的demo操作界面如下:

支持的浏览器

  1. 基于Xhr2和File API,可以支持IE10+, Firefox,Chrome和Opera最新版。
  2. 借助PostObject接口,可以支持IE低版本(6,7,8,9),详细请参见进阶篇二:通过PostObject接口处理IE低版本

支持的配置参数

名称 是否必填 默认值 说明
bos_bucket Y 需要上传到的Bucket
browse_button Y 需要初始化的<input type="file"/>
uptoken_url N 用来进行计算签名的URL,需要支持JSONP
bos_endpoint N http://bj.bcebos.com BOS服务器的地址
bos_ak N 如果没有设置uptoken_url的话,必须有ak和sk这个配置才可以工作
bos_sk N 如果没有设置uptoken_url的话,必须有ak和sk这个配置才可以工作
uptoken N 如果是临时的ak和sk,必须通过这个参数设置sts token的值
multi_selection N false 是否可以选择多个文件
auto_start N false 选择文件之后,是否自动上传
max_file_size N 100M 可以选择的最大文件,超过这个值之后,会被忽略掉
bos_multipart_min_size N 10M 超过这个值之后,采用分片上传的策略。如果想让所有的文件都采用分片上传,把这个值设置为0即可
chunk_size N 4M 分片上传的时候,每个分片的大小(如果没有切换到分片上传的策略,这个值没意义)
accept N 可以支持选择的文件类型,以逗号分割的后缀名,例如:txt,pdf,doc,docx
flash_swf_url N mOxie Flash文件的地址。如果需要支持低版本的IE,必须设置这个参数

更详细的使用信息请参阅:bce-bos-uploader说明

bce-bos-upload支持默认、STS、PostObject三种签名方式。

  • 默认签名方式即使用AK/SK签名方式在浏览器中直接上传文件到BOS中。通过浏览器直传文件到BOS服务器的时候,如果把AK和SK暴露在页面中,会引发安全性的问题。 攻击者如果获取了AK和SK,可以对BOS上面的数据进行任意的操作,为了降低泄露的风险,建议用户使用STS临时认证。
  • 使用STS签名更安全灵活,它可以对用户的使用权限进行灵活、精确地控制,而且不必每次请求都调用后端接口,在有效期内就可以不用再请求新的Security Token。
  • 因为IE低版本(IE6,7,8,9)对 HTML5 API 支持的不完善,为了在这些浏览器里面实现文件直传的功能,BOS开发了PostObject接口,支持了 multipart/form-data 的请求格式,方便在低版本的IE下面把文件上传到BOS服务器。

基础篇:在浏览器中直接上传文件到BOS

使用 bce-bos-uploader,可以参考下面的内容完成如何在浏览器中直接上传文件到BOS。使用流程:

  1. 开启Bucket的跨域访问设置
  2. 查询ak/sk
  3. 初始化bce-bos-uploader参数

开启Bucket的跨域访问

受浏览器安全限制,如果想直接在浏览器中访问BOS服务,必须正确设置好相关bucket的跨域功能。设置方法如下:

  1. 登录百度智能云控制台。
  2. 选择Bucket并进入Bucket管理页面。
  3. 点击左侧『Bucket属性』,进入Bucket配置的页面。
  4. 点击右侧『CORS设置』,进入CORS设置页面。
  5. 点击『添加规则』按钮,可以添加一条或者多条CORS的规则。

查询AK/SK

在百度智能云控制台首页右上角账号下的“安全认证”查询AK和SK的信息,也可以在Bucket管理中查看。详细操作可参见管理ACCESSKEY

获取bce-bos-uploader

有两种方式可以获取bce-bos-uploader的代码:

  • 第一种:通过npm安装

    npm install @baiducloud/bos-uploader
    
  • 第二种:直接引用CDN上面的资源(测试专用,不建议用于生产环境)

    <script src=" https://bj.bcebos.com/v1/bce-cdn/lib/@baiducloud/bos-uploader/<version>/bce-bos-uploader.bundle.min.js"></script>
    

初始化bce-bos-uploader

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>bce-bos-uploader simple demo</title>
    <!--[if lt IE 8]><script src="https://unpkg.com/json3@3.3.2/lib/json3.min.js"></script><![endif]-->
    <!--[if lt IE 9]><script src="https://unpkg.com/js-polyfills@0.1.42/es5.js"></script><![endif]-->
    <!--[if lt IE 10]><script src="https://unpkg.com/mOxie@1.5.7/bin/js/moxie.min.js"></script><![endif]-->
    <script src="https://unpkg.com/jquery@3.3.1/dist/jquery.min.js"></script>
    <script src="https://bce.bdstatic.com/lib/@baiducloud/bos-uploader/1.4.0-rc.0/bce-bos-uploader.bundle.min.js"></script>
  </head>
  <body>
    <input type="file" id="file" >
    <button type="submit">开始上传</button>
    <script>
      var uploader = new baidubce.bos.Uploader({
      browse_button: '#file',
      bos_bucket: '<your bucket>',
      bos_endpoint: '<your host>',
      bos_ak: '<your ak>', 
      bos_sk: '<your sk>',
      max_file_size: '1Gb',
      init: {
        FileUploaded: function (_, file, info) {
          var bucket = info.body.bucket;
          var object = info.body.object;
          var url = '<your host>' + bucket + '/' + object;
          $(document.body).append($('<div><a href="' + url + '">' + url + '</a></div>'));
        },
        UploadComplete: function() {
          $(document.body).append('<div>上传结束!</div>');
        }
      }
    });
     $('button[type=submit]').click(function () {
      uploader.start();
      return false;
    });

    </script>
  </body>
</html>

将上述代码保存为index.html,下面会启动webserver来访问这个页面

启动webserver

  • 通过PHP来启动

    php -S 0.0.0.0:9999
    
  • 通过Python来启动

    python -m SimpleHTTPServer 9999
    
  • 其它方式请参考相关的文档

启动webserver之后,在浏览器里面访问http://localhost:9999/index.html打开刚才的页面,开始测试是否可以正常上传。

进阶篇一:STS临时认证

Bce-bos-uploader支持STS(Security Token Service)临时授权的方式。服务端生成一组具体特定操作权限、具有一定时效性的临时AK/SK,这组临时的AK/SK可以暴露给浏览器端直接使用。用户只需要将服务端返回的AK/SK及SessionToken设置为bce-bos-uploader对应的bos-ak、bos-sk和uptoken参数。
下图简单介绍了整个业务交互过程,关于STS方面的介绍请参考临时授权访问

image.png

代码实现分为应用服务器端和客户端两部分,实现过程如下:

  1. 配置应用服务器端,以Nodejs实现为例,服务器端会返回AK/SK/SessionToken。
  2. 配置浏览器,根据服务器端返回的AK/SK/SessionToken初始化bce-bos-uploader参数。

应用服务器端Nodejs实现

var http = require('http');
var url = require('url');
var util = require('util');

var STS = require('@baiducloud/sdk').STS;

var kCredentials = {
    ak: '您的AK',
    sk: '您的SK'
};

function buildStsResponse() {
    var stsClient = new STS({
        credentials: kCredentials,
        region: 'bj'
    });
    return stsClient.getSessionToken(60 * 60 * 24, {
        accessControlList: [{
            service: 'bce:bos',
            resource: ['bce-javascript-sdk-demo-test'],
            region: '*',
            effect: 'Allow',
            permission: ['READ', 'WRITE']
        }]
    }).then(function (response) {
        var body = response.body;
        return {
            AccessKeyId: body.accessKeyId,
            SecretAccessKey: body.secretAccessKey,
            SessionToken: body.sessionToken,
            Expiration: body.expiration
        };
    });
}

http.createServer(function (req, res) {
    console.log(req.url);

    var query = url.parse(req.url, true).query;

    var promise = null;

    if (query.sts) {
        promise = buildStsResponse();
    }

    promise.then(function (payload) {
        res.writeHead(200, {
            'Content-Type': 'text/javascript; charset=utf-8',
            'Access-Control-Allow-Origin': '*'
        });

        if (query.callback) {
            res.end(util.format('%s(%s)', query.callback, JSON.stringify(payload)));
        }
        else {
            res.end(JSON.stringify(payload));
        }
    });
}).listen(1337);
console.log('Server running at http://0.0.0.0:1337/');

在服务器端,用与创建bosClient实例类似的方式创建一个stsClient实例。对于stsClient实例,主要有一个方法,那就是getSessionToken。这个方法接收两个参数,第一个参数是临时授权的有效期,以秒为单位;第二个单位是具体的权限控制,参见STS服务接口

这个方法会异步访问STS授权服务器,返回一个promise对象。STS授权服务器会返回类似如下内容:

{   
    body: {         
        "accessKeyId": "d87a16e5ce1d47c1917b38ed03fbb329", 
        "secretAccessKey": "e9b6f59ce06c45cdaaea2296111dab46",
         "sessionToken": "MjUzZjQzNTY4OTE0NDRkNjg3N2E4YzJhZTc4YmU5ZDh8AAAAABwCAAB/HfHDVV2bu5xUf6rApt2YdSLG6+21UTC62EHvIuiaamtuMQQKNkR9PU2NJGVbuWgBn8Ot0atk0HnWYQGgwgyew24HtbrX3GFiR/cDymCowm0TI6OGq7k8pGuBiCczT8qZcarH7VdZBd1lkpYaXbtP7wQJqiochDXrswrCd+J/I2CeSQT6mJiMmvupUV06R89dWBL/Vcu7JQpdYBk0d5cp2B+gdaHddBobevlBmKQw50/oOykJIuho4Wn7FgOGPMPdod0Pf0s7lW/HgSnPOjZCgRl0pihs197rP3GWpnlJRyfdCY0g0GFG6T0/FsqDbxbi8lWzF1QRTmJzzh2Tax8xoPFKGMbpntp//vGP7oPYK1JoES34TjcdcZnLzIRnVIGaZAzmZMUhPEXE5RVX1w8jPEXMJJHSrFs3lJe13o9Dwg==",         
        "createTime": "2016-02-16T14:01:29Z",         
        "expiration": "2016-02-16T15:41:29Z",         
        "userId": "5e433c4a8fe74765a7ec6fc147e25c80"     
    } 
}

服务器端需要把accessKeyId、secretAccessKey、sessionToken三个字段下发给浏览器端。

配置浏览器端bce-bos-uploader参数

使用STS临时授权机制时,只需要在各个服务初始化的时候把上面所说的参数accessKeyId、secretAccessKey、sessionToken引入就可以了。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>bce-bos-uploader simple demo</title>
    <!--[if lt IE 8]><script src="http://websdk.cdn.bcebos.com/bos/json3/lib/json3.min.js"></script><![endif]-->
    <!--[if lt IE 9]><script src="http://websdk.cdn.bcebos.com/bos/js-polyfills/es5.js"></script><![endif]-->
    <!--[if lt IE 10]><script src="http://websdk.cdn.bcebos.com/bos/moxie/bin/js/moxie.js"></script><![endif]-->
    <script src="http://websdk.cdn.bcebos.com/bos/jquery/dist/jquery.min.js"></script>
    <script src="http://websdk.cdn.bcebos.com/bos/bce-bos-uploader/bce-bos-uploader.bundle.js"></script>
  </head>
  <body>

    <input type="file" id="file" >
    <script>
    var uploader = new baidubce.bos.Uploader({
      browse_button: '#file',
      bos_bucket: '<your bucket>',
      bos_endpoint: 'http://bj.bcebos.com',
      bos_ak: '<your ak>', 
      bos_sk: '<your sk>',
      uptoken: '<your sessionToken>'
    });
    </script>
  </body>
</html>

进阶篇二:通过PostObject接口处理IE低版本

因为IE低版本(IE8,IE9)对html5支持的不完善,为了在这些浏览器里面实现文件直传的功能, BOS开发了PostObject接口,通过一个multipart/form-data的格式,就可以把文件上传到BOS服务器。Postobject接口的签名模式下应用服务器端对policy生成签名,再返回给客户端。

bce-bos-uploader已经实现了对这个接口的支持,使用之前需要进行额外的配置工作:

配置应用服务器端

  1. 上传crossdomain.xml

基于html5的跨域方案,我们需要设置跨域范文(CORS);如果通过flash来完成跨域数据交互的话,需要设置crossdomain.xml,可以直接把如下内容保存为crossdomain.xml,然后上传到Bucket的根目录。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <site-control permitted-cross-domain-policies="all"/>
  <allow-access-from domain="*" secure="false" />
  <allow-http-request-headers-from domain="*" headers="*" secure="false"/>
</cross-domain-policy>

如果bucket是private的,需要设置bucket为自定义权限来允许crossdomain.xml为public-read。在Console控制台选择bucket并进入“基础设置”页签,选择“Bucket权限设置”对应的“修改配置”,修改bucket的权限设置为“自定义权限”并添加权限。

image.png

  1. 服务器端返回uptoken_url参数。

使用PostObject处理IE低版本时,需要在bce-bos-uploader中配置uptoken_url参数。

配置浏览器端bce-bos-uploader参数

  • 以html方式初始化bce-bos-uploader中的uptoken_url参数。
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>bce-bos-uploader simple demo</title>
    <!--[if lt IE 8]><script src="./bower_components/json3/lib/json3.min.js"></script><![endif]-->
    <!--[if lt IE 9]><script src="./bower_components/js-polyfills/es5.js"></script><![endif]-->
    <!--[if lt IE 10]><script src="./bower_components/moxie/bin/js/moxie.js"></script><![endif]-->
    <script src="./bower_components/jquery/dist/jquery.min.js"></script>
    <script src="./bower_components/bce-bos-uploader/bce-bos-uploader.bundle.js"></script>
  </head>
  <body>
    <input type="file" id="file"
           data-multi_selection="true"
           data-bos_bucket="baidubce"
           data-uptoken_url="http://127.0.0.1:1337/ack" />
    <script>new baidubce.bos.Uploader('#file');</script>
  </body>
</html>
  • 以js方式初始化bce-bos-uploader中的uptoken_url参数。
<input type="file" id="file">
<script>
var uploader = new baidubce.bos.Uploader({
  browse_button: '#file',
  bos_bucket: 'baidubce',
  multi_selection: true,
  uptoken_url: 'http://127.0.0.1:1337/ack'
});
</script>

原理篇一:在浏览器中直接上传文件到BOS

如您不使用bce-bos-uploader,可以参考下面的内容完成如何在浏览器中直接上传文件到BOS。使用流程:

  1. 开启Bucket的跨域访问设置
  2. 查询ak/sk
  3. 初始化BosClient
  4. 处理上传逻辑

开启Bucket的跨域访问

受浏览器安全限制,如果想直接在浏览器中访问BOS服务,必须正确设置好相关bucket的跨域功能。设置方法请参考开启Bucket的跨域访问

初始化设置

var bosConfig = {
    credentials: {
         ak: '从百度智能云控制台查询您的ak',
         sk: '从百度智能云控制台查询上面这个ak所对应的sk'
     },
     endpoint: 'http://bj.bcebos.com' // 根据您选用bos服务的区域配置相应的endpoint
 };
 var bucket = 'bce-javascript-sdk-demo-test'; // 设置您想要操作的bucket
 var client = new baidubce.sdk.BosClient(bosConfig);

后续我们可以使用client这个实例来进行BOS相关操作。

上传逻辑

我们可以通过调用client.putObjectFromBlob(bucket, key, blob, options)来完成文件的上传操作。

这个函数支持4个参数,其中options是可选的。如果需要手工设置文件的的Content-Type,可以放到options参数里面。 如果没有手工设置,默认的Content-Type是application/oceat-stream。
另外,可以通过调用baidubce.sdk.MimeType.guess(ext)来根据后缀名得到一些常用的Content-Type。

注意:因为Firefox兼容性的一个问题,如果上传的文件是 text/* 类型,Firefox 会自动添加 charset=utf-8 因此我们给 options 设置 Content-Type 的时候,需要手工加上 charset=utf-8,否则会导致浏览器计算的签名跟服务器计算的签名不一致,导致上传失败。

// 监听文件上传的事件,假设页面中有:<input type="file" id="upload" /> $('#upload').on('change', function (evt) {
     var file = evt.target.files[0]; // 获取要上传的文件
     var key = file.name; // 保存到bos时的key,您可更改,默认以文件名作为key
     var blob = file;

     var ext = key.split(/\./g).pop();
     var mimeType = baidubce.sdk.MimeType.guess(ext);
     if (/^text\//.test(mimeType)) {
         mimeType += '; charset=UTF-8';
     }
     var options = {
         'Content-Type': mimeType
     };

      client.putObjectFromBlob(bucket, key, blob, options)
         .then(function (res) {
             // 上传完成,添加您的代码
             console.log('上传成功');
         })
         .catch(function (err) {
             // 上传失败,添加您的代码
             console.error(err);
         });
  });

如果想获悉当前上传的进度,可以监听progress事件。

client.on('progress', function (evt)
 {
     // 监听上传进度
     if (evt.lengthComputable)
 {
         // 添加您的代码
         var percentage = (evt.loaded / evt.total) * 100;
         console.log('上传中,已上传了' + percentage + '%');
     }
 });

原理篇二:大文件分块上传

用户在使用浏览器上传文件到BOS的时候,如果遇到文件过大,需要先将文件分块然后再上传。上传过程中有可能会遇到页面关闭、浏览器崩溃、网络连接中断等问题,从而导致上传失败。BOS支持分块上传和断点续传功能。分块上传请参见“Object的分块上传”,下面介绍“断点续传”的实现方法。

实现原理

在我们使用文件分块上传(multipartUpload)的时候,BOS首先会为这个上传过程分配一个uploadId。然后我们将一个文件被分成了若干part,每个part独立上传,上传完成后,BOS 服务会为这个part生成一个eTag。当所有part都上传完成的时候,BOS 服务根据这些eTag和uploadId把正确的part找出来,并组合成原本的文件。

在这个过程中,BOS 并不需要所有的part一下子全部上传完毕,而是可以分多次进行。这也就是就,上传过程中,当页面意外关闭时,我们可以不必从头开始重新上传,而只需要把未上传成功的part的再次上传就可以。当然,前提是我们需要把此次上传的uploadId和上传完成的part的etag保存下来(不过,更推荐的做法是通过listParts接口来查询更精确的已上传分块信息)。在上传一个part之前,可以先检查一下,这个part是否已经上传过了,如果以前已上传成功,那就直接跳过这个part的上传过程。

对于uploadId的存储,需要满足不受页面关闭的影响,比较理想的做法是存储在localStorage中。

本地存储

在保存uploadId时,我们需要为它指定一个key,让不同的文件、不同的上传过程区分开。本示例采用文件名、文件大小、分区大小、bucket名称、object名称组合成这个key:

var generateLocalKey = function (blob, chunkSize, bucket, object) {
     return [blob.name, blob.size, chunkSize, bucket, object].join('&');
 };

注意:用这个方式生成的key并不准确,如果两次上传过程中,选择了两个文件名相同、文件大小相同,但内容不同的文件,那么用这样的方式并不能正确区分这两个文件。更严谨的方式是根据文件名和文件内容计算MD5,并以此为key。

存储方式我们选择localStorage:

var getUploadId = function (key) {
     return localStorage.getItem(key);
 };  
var setUploadId = function (key, uploadId) {
     return localStorage.setItem(key, uploadId);
 };  
var removeUploadId = function (key) {
     return localStorage.removeItem(key);
 };

初始化分块上传

在初始化分块上传时,有两种可能:

  • 如果已经存在此文件的uploadId,那么跳过initiateMultipartUpload()方法,改为调用listParts()来获取已上传分块信息;
  • 如果没有此文件的uploadId,那么调用initiateMultipartUpload()方法获得新的uploadId,并将这个uploadId保存在localStorage中。
// ...省略BosClient初始化过程
// var bosClient = new BosClient(bosConfig);

  var initiateMultipartUpload = function (file, chunkSize, bucket, object) {
     // 根据文件生成localStorage的key
     var key = generateLocalKey(file, chunkSize, bucket, object);
      // 获取对应的`uploadId`
     var uploadId = getUploadId(key);
      if (uploadId) {
         // `uploadId`存在,说明有未完成的分块上传。
         // 那么调用`listParts()`获取已上传分块信息。
         return BosClient.listParts(bucket, object, uploadId)
             .then(function (response) {
                 // response.body.parts里包含了已上传分块的信息
                 response.body.uploadId = uploadId;
                 return response;
             });
     }
     else {
         // `uploadId`不存在,那么用正常的流程初始化
         return BosClient.initiateMultipartUpload(bucket, object)
             .then(function (response) {
                 // response.body.uploadId为新生成的`uploadId`
                 response.body.parts = [];
                  // 为了下次能使用断点续传,我们需要把新生成的`uploadId`保存下来
                 setUploadId(key, response.body.uploadId);
                 return response;
             });
     }
 }

分块上传

在对大文件分割分块时,我们可以跟以上传的分块列表进行比较,以确定是否需要真的进行上传。

function getEtag(partNumber, parts){
     // 从已上传part列表中找出特定partNumber的part的eTag
     for(var i = 0, l = parts.length; i < l; i++){
         if (parts[i].partNumber === partNumber) {
             return parts[i].eTag;
         }
     }
     return null;
 }
  function getTasks (file, uploadId, chunkSize, bucket, object, parts) {
     var leftSize = file.size;
     var offset = 0;
     var partNumber = 1;

     var tasks = [];

     while (leftSize > 0) {
         var partSize = Math.min(leftSize, chunkSize);
         var task = {
             file: file,
             uploadId: uploadId,
             bucket: bucket,
             object: object,
             partNumber: partNumber,
             partSize: partSize,
             start: offset,
             stop: offset + partSize - 1
         };

          // 如果在已上传完成的分块列表中找到这个分块的etag,那么记录下来
         var etag = getEtag(partNumber, parts);
         if (etag){
             task.etag = etag;
         }
          tasks.push(task);
          leftSize -= partSize;
         offset += partSize;
         partNumber += 1;
     }
      return tasks;
 }

在进行分块上传处理的时候,根据是否已带有etag字段来决定是否需要上传:

function uploadPartFile(state, bosClient) {
     return function(task, callback) {
         if (task.etag) {
             // 如果有etag字段,则直接跳过上传
             callback(null, {
                 http_headers: {
                     etag: task.etag
                 },
                 body: {}
             });
         }
         else {
             // 否则进行上传
             var blob = task.file.slice(task.start, task.stop + 1);
             bosClient.uploadPartFromBlob(task.bucketName, task.key, task.uploadId, task.partNumber, task.partSize, blob)
                 .then(function(res) {
                     ++state.loaded;
                    callbacknull,res);
                 })
                 .catch(function(err) {
                     callback(err);
                 });
         }
     };
 }

流程代码

我们对每个步骤的代码做了一些小修改,但整个流程的代码与分块上传很类似:

var chunkSize = 5 * 1024 * 1024; // 分块大小
var uploadId;
initiateMultipartUpload(file, chunkSize, bucket, object)
     .then(function(response) {
         uploadId = response.body.uploadId; // uploadId,可能是服务器刚刚生成的,也可能是从localStorage获取的
         var parts = response.body.parts || []; // 已上传的分块列表。如果是新上传,则为空数组
         var deferred = sdk.Q.defer();
         var tasks = getTasks(blob, uploadId, chunkSize, bucket, key, parts);
         var state = {
             lengthComputable: true,
             loaded: parts.length, // 已上传的分块数
             total: tasks.length
         }; 
         // 如果已上传的分块数大于0,可以先修改一下文件上传进度
         bosClient.emit('progress', state);
         // 为了管理分块上传,使用了async(https://github.com/caolan/async)库来进行异步处理
         var THREADS = 2; // 同时上传的分块数量
         async.mapLimit(tasks, THREADS, uploadPartFile(state, bosClient), function(err, results) {
             if (err) {
                 deferred.reject(err);
             } else {
                 deferred.resolve(results);
             }
         });
         return deferred.promise;
     })
     .then(function(allResponse) {
         var partList = [];
         allResponse.forEach(function(response, index) {
             // 生成分块清单
             partList.push({
                 partNumber: index + 1,
                 eTag: response.http_headers.etag
             });
         }); 

         // 所有分块上传完成后,可以删除对应的`uploadId`了
         removeUploadId(key, uploadId);

         return bosClient.completeMultipartUpload(bucket, key, uploadId, partList); // 完成上传
     })
     .then(function (res) {
         // 上传完成
     })
     .catch(function (err) {
         // 上传失败,添加您的代码
         console.error(err);
     });

原理篇三:STS临时认证

Bce-bos-uploader支持STS(Security Token Service)临时授权的方式。服务端生成一组具体特定操作权限、具有一定时效性的临时AK/SK,这组临时的AK/SK可以暴露给浏览器端直接使用。用户只需要将服务端返回的AK/SK及SessionToken设置为bce-bos-uploader对应的bos-ak、bos-sk和uptoken参数。

关于STS方面的介绍请参考临时授权访问。使用流程:

  1. 配置应用服务器端Nodejs实现
  2. 获取临时AK/SK/SessionToken
  3. 初始化bce-bos-uploader参数

应用服务器端Nodejs实现

var http = require('http');
var url = require('url');
var util = require('util');

var STS = require('@baiducloud/sdk').STS;

var kCredentials = {
    ak: '您的AK',
    sk: '您的SK'
};

function buildStsResponse() {
    var stsClient = new STS({
        credentials: kCredentials,
        region: 'bj'
    });
    return stsClient.getSessionToken(60 * 60 * 24, {
        accessControlList: [{
            service: 'bce:bos',
            resource: ['bce-javascript-sdk-demo-test'],
            region: '*',
            effect: 'Allow',
            permission: ['READ', 'WRITE']
        }]
    }).then(function (response) {
        var body = response.body;
        return {
            AccessKeyId: body.accessKeyId,
            SecretAccessKey: body.secretAccessKey,
            SessionToken: body.sessionToken,
            Expiration: body.expiration
        };
    });
}

http.createServer(function (req, res) {
    console.log(req.url);

    var query = url.parse(req.url, true).query;

    var promise = null;

    if (query.sts) {
        promise = buildStsResponse();
    }

    promise.then(function (payload) {
        res.writeHead(200, {
            'Content-Type': 'text/javascript; charset=utf-8',
            'Access-Control-Allow-Origin': '*'
        });

        if (query.callback) {
            res.end(util.format('%s(%s)', query.callback, JSON.stringify(payload)));
        }
        else {
            res.end(JSON.stringify(payload));
        }
    });
}).listen(1337);
console.log('Server running at http://0.0.0.0:1337/');

在服务器端,用与创建bosClient实例类似的方式创建一个stsClient实例。对于stsClient实例,主要有一个方法,那就是getSessionToken。这个方法接收两个参数,第一个参数是临时授权的有效期,以秒为单位;第二个单位是具体的权限控制,参见STS服务端口

这个方法会异步访问STS授权服务器,返回一个promise对象。STS授权服务器会返回类似如下内容:

{   
    body: {         
        "accessKeyId": "d87a16e5ce1d47c1917b38ed03fbb329",
        "secretAccessKey": "e9b6f59ce06c45cdaaea2296111dab46", 
        "sessionToken": "MjUzZjQzNTY4OTE0NDRkNjg3N2E4YzJhZTc4YmU5ZDh8AAAAABwCAAB/HfHDVV2bu5xUf6rApt2YdSLG6+21UTC62EHvIuiaamtuMQQKNkR9PU2NJGVbuWgBn8Ot0atk0HnWYQGgwgyew24HtbrX3GFiR/cDymCowm0TI6OGq7k8pGuBiCczT8qZcarH7VdZBd1lkpYaXbtP7wQJqiochDXrswrCd+J/I2CeSQT6mJiMmvupUV06R89dWBL/Vcu7JQpdYBk0d5cp2B+gdaHddBobevlBmKQw50/oOykJIuho4Wn7FgOGPMPdod0Pf0s7lW/HgSnPOjZCgRl0pihs197rP3GWpnlJRyfdCY0g0GFG6T0/FsqDbxbi8lWzF1QRTmJzzh2Tax8xoPFKGMbpntp//vGP7oPYK1JoES34TjcdcZnLzIRnVIGaZAzmZMUhPEXE5RVX1w8jPEXMJJHSrFs3lJe13o9Dwg==",         
        "createTime": "2016-02-16T14:01:29Z",         
        "expiration": "2016-02-16T15:41:29Z",         
        "userId": "5e433c4a8fe74765a7ec6fc147e25c80"     
    } 
}

服务器端需要把accessKeyId、secretAccessKey、sessionToken三个字段下发给浏览器端。

浏览器前端实现

前端使用STS临时授权机制时,只需要在各个服务初始化的时候把上面所说的参数accessKeyId、secretAccessKey、sessionToken引入就可以了。以BOS为例:

var bosConfig = {
     credentials: {
         ak: '{accessKeyId}', // STS服务器下发的临时ak
         sk: '{secretAccessKey}' // STS服务器下发的临时sk
     },
     sessionToken: '{sessionToken}',  // STS服务器下发的sessionToken
     endpoint: 'http://bj.bcebos.com'
 };
var client = new baidubce.sdk.BosClient(bosConfig);