尝试分段上传时 AWS.S3.upload() 403 错误

AJB*_*AJB 3 upload multipart amazon-s3 amazon-web-services aws-sdk-js

长话短说

当尝试使用浏览器中适用于 Javascript 的 AWS 开发工具包s3.upload()提供的方法以及通过调用生成的临时IAM 凭证 直接从浏览器上传文件时,对于非分段上传以及分段上传的第一部分,一切正常上传。AWS.STS.getFederationToken()

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

为什么?



上下文

我正在我的应用程序中实现一个上传器,它将支持直接从浏览器到我的 S3 存储桶的分段(分块)上传。

为了实现这一目标,我在浏览器中使用了适用于 Javascript 的 AWS 开发工具包s3.upload()的方法,据我所知,它只不过是其对.new AWS.S3.ManagedUpload()

我正在尝试的简单说明可以在这里找到:https://aws.amazon.com/blogs/developer/announcing-the-amazon-s3-management-uploader-in-the-aws-sdk-for- javascript/

此外,我还使用API 层AWS.STS.getFederationToken()提供临时IAM 用户凭证来授权上传。

1、2、3:

  1. 用户通过标准 HTML 选择文件来启动上传<input type="file">
  2. 这会触发对我的 API 层的初始请求,以确保用户在我自己的系统上拥有执行此操作所需的权限,如果这是真的,那么我的服务器会使用AWS.STS.getFederationToken()一个Policy参数进行调用,该参数将其权限范围限制为将文件上传到提供钥匙。然后将生成的临时信用返回给浏览器。
  3. 现在浏览器已经有了它需要的临时信用,它可以使用它们来创建一个新的AWS.S3客户端,然后执行该AWS.S3.upload()方法来执行文件的(据称)自动分段上传。



代码

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);

    });

}
Run Code Online (Sandbox Code Playgroud)


console.myapp.com/uploader.js

这是浏览器端上传程序的截断图,它首先调用vendUploadCredsAPI 方法,然后使用生成的临时凭据来执行分段上传。

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);
    });

}
Run Code Online (Sandbox Code Playgroud)


cdn.myapp.com S3 存储桶 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>
Run Code Online (Sandbox Code Playgroud)


错误

好吧,当我尝试上传文件时,它变得非常混乱:

  1. 任何 5Mb 以下的文件都可以上传。5Mb 以下的文件(S3 分段上传的最小部分大小)不需要分段上传,因此s3.upload()将它们作为标准 PUT 请求发送。有道理,他们也成功了。
  2. 任何超过 5Mb 的文件似乎都能很好地上传,但仅限于第一部分。然后,当s3.upload()尝试发送第二部分时,S3 会返回错误403 Access Denied

我希望您是信息迷,因为这是我尝试上传 Astrud Gilberto 的忧郁经典“So Nice (Summer Samba)”(MP3,6.6Mb)时从 Chrome 收到的错误转储:

一般的

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
Run Code Online (Sandbox Code Playgroud)

响应头

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
Run Code Online (Sandbox Code Playgroud)

请求标头

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
Run Code Online (Sandbox Code Playgroud)

查询字符串参数

partNumber:2
uploadId:ljaviv9n25aRKwc4HKGhBbbXTWI3wSGZwRRi39fPSEvU2dcM9G7gO6iu5w7va._dMTZil4e_b53Iy5ngojJqRr5F6Uo_ZXuF27yaqizeARmUVf5ZVeah8ZjYwkZV8C0i3rhluYoxFHUPxlLMjaKLww--
Run Code Online (Sandbox Code Playgroud)

实际响应主体

以下是 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>
Run Code Online (Sandbox Code Playgroud)


问题

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

AJB*_*AJB 5

一个办法

经过几个小时的思考,我发现问题出在我作为请求参数发送的IAM 策略Condition块上。具体来说,仅发送第一个请求的标头,即对.PolicyAWS.STS.getFederationToken()AWS.S3.upload()x-amz-acl PUTS3.initiateMultipartUpoad

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

我的IAM 策略有以下条件,我用它来确保任何上传都必须具有“私有”ACL:

Condition : {
    StringEquals : {
        's3:x-amz-acl' : ['private']
    }
}
Run Code Online (Sandbox Code Playgroud)

所以最初的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
            ]
        }
    ]
};
Run Code Online (Sandbox Code Playgroud)

希望这能帮助其他人避免在此浪费三天时间。