尝试分段上传

时间:2017-12-10 11:45:33

标签: amazon-web-services amazon-s3 upload multipart aws-sdk-js

TL; DR

尝试使用浏览器中适用于Javascript的AWS开发工具包提供的s3.upload()方法直接从浏览器上传文件,并结合使用时生成的临时 IAM凭据致电AWS.STS.getFederationToken()一切都适用于非分段上传,以及分段上传的第一个部分。

但是当s3.upload()尝试发送分段上传的第二部分 S3 时会发出403 Access Denied错误。

为什么?



上下文

我在我的应用程序中实现了一个上传器,它可以直接从浏览器到我的S3存储桶上进行多部分(分块)上传。

为实现这一目标,我使用了s3.upload() AWS SDK for Javascript in the Browsernew AWS.S3.ManagedUpload()方法,据我所知,它仅仅是因为https://aws.amazon.com/blogs/developer/announcing-the-amazon-s3-managed-uploader-in-the-aws-sdk-for-javascript/的基础利用而使用的糖。

我可以在此处找到我尝试的简单说明:AWS.STS.getFederationToken()

此外,我还使用Policy param作为从我的API层销售临时 IAM用户凭据以授权上传的方法。

1,2,3:

  1. 用户通过标准HTML <input type="file">选择文件来启动上传。
  2. 这会触发对我的API层的初始请求,以确保用户在我自己的系统上具有执行此操作所需的权限,如果这是真的,那么我的服务器调用AWS.STS.getFederationToken() {{3}将其权限范围仅限于将文件上传到提供的密钥。然后将生成的临时信用返回给浏览器。
  3. 既然浏览器具有所需的临时信誉,它可以使用它们来创建新的AWS.S3客户端,然后执行AWS.S3.upload()方法来执行(假设的)自动分段上传文件。


  4. 代码

    api.myapp.com/vendUploadCreds.js

    这是调用的API层方法,用于生成和销售临时上载信用。在此过程中,帐户已经过身份验证并被授权接收信用并上传文件。

    module.exports = function vendUploadCreds(request, response) {
    
        var account = request.params.account;
        var file = request.params.file;
        var bucket = 'cdn.myapp.com';
    
        var sts = new AWS.STS({
            AccessKeyId : process.env.MY_AWS_ACCESS_KEY_ID,
            SecretAccessKey : process.env.MY_AWS_SECRET_ACCESS_KEY
        });
    
        /// The following policy is *exactly* the same as the S3 policy
        /// attached to the IAM user that executes this STS request.
    
        var policy = {
            Version : '2012-10-17',
            Statement : [
                {
                    Effect : 'Allow',
                    Action : [
                        's3:ListBucket',
                        's3:ListBucketMultipartUploads',
                        's3:ListBucketVersions',
                        's3:ListMultipartUploadParts',
                        's3:AbortMultipartUpload',
                        's3:GetObject',
                        's3:GetObjectVersion',
                        's3:PutObject',
                        's3:PutObjectAcl',
                        's3:PutObjectVersionAcl',
                        's3:DeleteObject',
                        's3:DeleteObjectVersion'
                    ],
                    Resource : [
                        'arn:aws:s3:::' + bucket + '/' + account._id + '/files/' + file.name
                    ],
                    Condition : {
                        StringEquals : {
                            's3:x-amz-acl' : ['private']
                        }
                    }
                }
            ]
        };
    
        sts.getFederationToken({
            DurationSeconds : 129600, /// 36 hours
            Name : account._id + '-uptoken',
            Policy : JSON.stringify(policy)
        }, function(err, data) {
    
            if (err) console.log(err, err.stack); // an error occurred
    
            response.send(data);
    
        });
    
    }
    


    console.myapp.com/uploader.js

    这是浏览器端上传者的截断图示,首先调用vendUploadCreds API方法,然后使用生成的临时信用卡执行分段上传。

    uploader.getUploadCreds(account, file) {
    
        /// A request is sent to api.myapp.com/vendUploadCreds
        /// Upon successful response, the creds are returned.
    
        request('https://api.myapp.com/vendUploadCreds', {
            params : {
                account : account,
                file : file
            }
        }, function(error, data) {
            upload.credentials = data.credentials;
            this.uploadFile(upload);
        });
    
    }
    
    uploader.uploadFile : function(upload) {
    
        var uploadID = upload.id;
    
        /// The `upload` object coming through via the args has
        /// a `credentials` property containing the creds obtained
        /// via the `vendUploadCreds` method above.
    
        var credentials = new AWS.Credentials({
            accessKeyId : upload.credentials.AccessKeyId,
            secretAccessKey : upload.credentials.SecretAccessKey,
            sessionToken : upload.credentials.SessionToken
        });
    
        AWS.config.region = 'us-east-1';
    
        var s3 = new AWS.S3({
            credentials,
            signatureVersion : 'v2', /// 'v4' also attempted
            params : {
                Bucket : 'cdn.myapp.com'
            }
        });
    
        var uploader = s3.upload({
            Key : upload.key,
            ACL : 'private',
            ContentType : upload.file.type,
            Body : upload.file
        },{
            queueSize : 3,
            partSize : 1024 * 1024 * 5
        });
    
        uploader.on('httpUploadProgress', function(event) {
            var total = event.total;
            var loaded = event.loaded;
            var percent = loaded / total;
            percent = Math.ceil(percent * 100);
            console.log('Uploaded ' + percent + '% of ' + upload.key);
        });
    
        uploader.send(function(error, result) {
            console.log(error, result);
        });
    
    }
    


    cdn.myapp.com S3 Bucket CORS配置

    据我所知,这是开放性的,所以CORS不应该成为问题吗?

    <?xml version="1.0" encoding="UTF-8"?>
    <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <ExposeHeader>ETag</ExposeHeader>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
    </CORSConfiguration>
    


    错误

    好的,所以当我尝试上传文件时,它会让人感到困惑:

    1. 5Mb以下的任何文件都上传得很好。低于5Mb的文件(S3分段上传的最小部件大小)不需要分段上传,因此s3.upload()将它们作为标准PUT请求发送。有道理,他们的成功就好了。
    2. 超过5Mb 的任何文件似乎上传正常,但仅限于第一部分。然后当s3.upload()尝试发送第二部分时,S3以403 Access Denied错误响应。
    3. 我希望你能成为信息的粉丝,因为当我试图上传Astrud Gilberto的忧郁经典"So Nice (Summer Samba)"时,我从Chrome获得的错误转移(MP3, 6.6MB):

      常规

      Request URL:https://s3.amazonaws.com/cdn.myapp.com/5a2cbda70b9b741661ad98df/files/Astrud-Gilberto-So-Nice-1512903188573.mp3?partNumber=2&uploadId=ljaviv9n25aRKwc4HKGhBbbXTWI3wSGZwRRi39fPSEvU2dcM9G7gO6iu5w7va._dMTZil4e_b53Iy5ngojJqRr5F6Uo_ZXuF27yaqizeARmUVf5ZVeah8ZjYwkZV8C0i3rhluYoxFHUPxlLMjaKLww--
      Request Method:PUT
      Status Code:403 Forbidden
      Remote Address:52.216.165.77:443
      Referrer Policy:no-referrer-when-downgrade
      

      响应标题

      Access-Control-Allow-Methods:GET, PUT, POST, DELETE
      Access-Control-Allow-Origin:*
      Access-Control-Expose-Headers:ETag
      Access-Control-Max-Age:3000
      Connection:close
      Content-Type:application/xml
      Date:Sun, 10 Dec 2017 10:53:12 GMT
      Server:AmazonS3
      Transfer-Encoding:chunked
      Vary:Origin, Access-Control-Request-Headers, Access-Control-Request-Method
      x-amz-id-2:0Mzo7b/qj0r5Is7aJIIJ/U2VxTTulWsjl5kJpTnEhy/B0fQDlRuANcursnxI71LA16AdePVSc/s=
      x-amz-request-id:DA008A5116E0058F
      

      请求标题

      Accept:*/*
      Accept-Encoding:gzip, deflate, br
      Accept-Language:en-US,en;q=0.9
      Authorization:AWS ASIAJAR5KXKAOPTC64PQ:Wo9lbflZuVVS9+UTTDSjU0iPUbI=
      Cache-Control:no-cache
      Connection:keep-alive
      Content-Length:1314943
      Content-Type:application/octet-stream
      DNT:1
      Host:s3.amazonaws.com
      Origin:http://132.12.23.145:8080
      Pragma:no-cache
      Referer:http://132.12.23.145:8080/
      User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
      X-Amz-Date:Sun, 10 Dec 2017 10:53:09 GMT
      x-amz-security-token:FQoDYXdzENT//////////wEaDK9srK2+5FN91W+T+SLSA/LdEwpOiY7wDkgggOMhuGEiqIXAQrFMk/EqvZFl8Npqx414WsL9E310rj5mU1RGXsxuN+ers1r6NVPpJIlXSDG7bnwlGabejNvDL9vMX5HJHGbZOEVUoaL60/T5NM+0TZtH61vHAEVmRVFKOB0tSez8TEU1jQ2cJME0THn5RuV/6CuIpA9dlEYO7/ajB5UKT3F1rBkt12b0DeWmKG2pvTJRwa8nrsF6Hk6dk1B1Hl1fUwAh9rD17O9Roi7MFLKisPH+96WX08liC8k+n+kPPOox6ZZM/lOMwlNinDjLc2iC+JD/6uxyAGpNbQ7OHAUsF7DOiMvw6Nv6PrImrBvnK439BhLOk1VXCfxxmtTWGim8TD1w1EciZcJhsuCMpDF8fMnhF/JFw3KNOJXHUtpTGRjNbOPcPojVs3FgIt+9MllIA0pGMr2bYmA3HvKewnhD2qeKkG3DPDIbpwuRoY4wIXCP5OclmoHp5nE5O94aRIvkBvS1YmqDQO+jTiI7/O7vlX63q9sGqdIA4nwzh5ASTRJhC2rKgxepFirEB53dCev8i9f1pwXG3/4H3TvPCLVpK94S7/csNJexJP75bPBpo4nDeIbOBKKIMuUDK1pQsyuGwuUolKS00QU=
      X-Amz-User-Agent:aws-sdk-js/2.164.0 callback
      

      查询字符串参数

      partNumber:2
      uploadId:ljaviv9n25aRKwc4HKGhBbbXTWI3wSGZwRRi39fPSEvU2dcM9G7gO6iu5w7va._dMTZil4e_b53Iy5ngojJqRr5F6Uo_ZXuF27yaqizeARmUVf5ZVeah8ZjYwkZV8C0i3rhluYoxFHUPxlLMjaKLww--
      

      实际响应主体

      这是S3的回应主体:

      <?xml version="1.0" encoding="UTF-8"?>
      <Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>8277A4969E955274</RequestId><HostId>XtQ2Ezv0Wa81Rm2jymB5ZwTe+OHfwTcnNapYMgceqZCJeb75YwOa1AZZ5/10CAeVgmfeP0BFXnM=</HostId></Error>
      


      问题

      1. sts.generateFederationToken()请求创建的信用卡显然不是问题,因为如果是,那么较小的(非多部分)上传也会失败,对吗?
      2. cdn.myapp.com存储桶上的CORS配置显然不是问题,因为如果是,那么较小的(非多部分)上传也会失败,对吗?
      3. 为什么S3会接受多部分上传的partNumber=1,然后在同一次上传的partNumber=2上接受403?

1 个答案:

答案 0 :(得分:2)

解决方案

经过几个小时的摔跤之后,我发现问题出在我发送的 IAM政策Condition块作为Policy param我的AWS.STS.getFederationToken()请求。具体而言,AWS.S3.upload()仅为第一个 x-amz-acl请求发送PUT标头,这是对S3.initiateMultipartUpoad的调用。

x-amz-acl标题 包含在后续PUT次上传的实际部分请求中。

我的 IAM政策有以下条件,我用它来确保所有上传的ACL必须为&#39; private&#39;:

Condition : {
    StringEquals : {
        's3:x-amz-acl' : ['private']
    }
}

因此,对PUT的初始S3.initiateMultipartUpload请求很好,但随后的PUT失败了,因为他们没有x-amz-acl标题。

解决方案是编辑我附加到临时用户的策略并将s3:PutObject权限移动到其自己的语句中,然后仅在目标值存在时调整条件才能应用。最终政策如下:

var policy = {
    Version : '2012-10-17',
    Statement : [
        {
            Effect : 'Allow',
            Action : [
                's3:PutObject'
            ],
            Resource : [
                'arn:aws:s3:::' + bucket + '/' + account._id + '/files/' + file.name
            ],
            Condition : {
                StringEqualsIfExists : {
                    's3:x-amz-acl' : ['private']
                }
            }
        },
        {
            Effect : 'Allow',
            Action : [
                's3:AbortMultipartUpload'
            ],
            Resource : [
                'arn:aws:s3:::' + bucket + '/' + account._id + '/files/' + file.name
            ]
        }
    ]
};

希望能帮助其他人浪费三天时间。