如何将客户端上传可查看文件到Amazon S3?

时间:2014-01-30 09:42:38

标签: node.js amazon-web-services amazon-s3

首先我要说的是,我通常非常不愿意发布这些问题,因为我一直觉得对互联网上的所有内容都有答案。在花了无数个小时寻找这个问题的答案之后,我终于放弃了这个陈述。

假设

这有效:

s3.getSignedUrl('putObject', params);

我想做什么?

  1. 使用getSignedUrl方法通过PUT(从客户端)将文件上传到Amazon S3
  2. 允许任何人查看上传到S3的文件
  3. 注意:如果有一种更简单的方法允许客户端(iPhone)使用预先签名的URL上传到Amazon S3(并且没有公开客户端的凭据),我会全力以赴。

    主要问题*

    1. 查看AWS管理控制台时,上传的文件设置了空白权限和元数据。
    2. 查看上传的文件时(即双击AWS管理控制台中的文件)我收到AccessDenied错误。
    3. 我尝试了什么?

      尝试#1:我的原始代码

      在NodeJS中,我生成一个预先签名的URL,如下所示:

      var params = {Bucket: mybucket, Key: "test.jpg", Expires: 600};
      s3.getSignedUrl('putObject', params, function (err, url){
        console.log(url); // this is the pre-signed URL
      });
      

      预签名网址如下所示:

      https://mybucket.s3.amazonaws.com/test.jpg?AWSAccessKeyId=AABFBIAWAEAUKAYGAFAA&Expires=1391069292&Signature=u%2BrqUtt3t6BfKHAlbXcZcTJIOWQ%3D
      

      现在我通过PUT上传文件

      curl -v -T myimage.jpg https://mybucket.s3.amazonaws.com/test.jpg?AWSAccessKeyId=AABFBIAWAEAUKAYGAFAA&Expires=1391069292&Signature=u%2BrqUtt3t6BfKHAlbXcZcTJIOWQ%3D
      

      问题
      我得到上面列出的 *主要问题

      尝试#2:在PUT上添加内容类型和ACL

      我也尝试在代码中添加Content-Type和x-amz-acl,方法是替换params:

      var params = {Bucket: mybucket, Key: "test.jpg", Expires: 600, ACL: "public-read-write", ContentType: "image/jpeg"};
      

      然后我尝试了一个很好的'PUT:

      curl -v -H "image/jpeg" -T myimage.jpg https://mybucket.s3.amazonaws.com/test.jpg?AWSAccessKeyId=AABFBIAWAEAUKAYGAFAA&Content-Type=image%2Fjpeg&Expires=1391068501&Signature=0yF%2BmzDhyU3g2hr%2BfIcVSnE22rY%3D&x-amz-acl=public-read-write
      

      问题
      我的终端输出了一些错误:

      -bash: Content-Type=image%2Fjpeg: command not found
      -bash: x-amz-acl=public-read-write: command not found
      

      我也得到了上面列出的 *主要问题

      尝试#3:修改存储桶权限为公开

      下面列出的所有项目都在AWS管理控制台中打勾

      Grantee: Everyone can [List, Upload/Delete, View Permissions, Edit Permissions]
      Grantee: Authenticated Users can [List, Upload/Delete, View Permissions, Edit Permissions]
      

      存储桶政策

      {
      "Version": "2012-10-17",
      "Statement": [
          {
              "Sid": "Stmt1390381397000",
              "Effect": "Allow",
              "Principal": {
                  "AWS": "*"
              },
              "Action": "s3:*",
              "Resource": "arn:aws:s3:::mybucket/*"
          }
      ]
      }
      

      尝试#4:设置IAM权限

      我将用户政策设置为:

      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": "*"
          }
        ]
      }
      

      AuthenticatedUsers组策略是这样的:

      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Sid": "Stmt1391063032000",
            "Effect": "Allow",
            "Action": [
              "s3:*"
            ],
            "Resource": [
              "*"
            ]
          }
        ]
      }
      

      尝试#5:设置CORS策略

      我将CORS政策设置为:

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

      而且......现在我在这里。

7 个答案:

答案 0 :(得分:10)

<强> 更新

我有新闻。根据SDK {2.1}在http://aws.amazon.com/releasenotes/1473534964062833发布的说明:

"The SDK will now throw an error if ContentLength is passed into an 
Amazon S3 presigned URL (AWS.S3.getSignedUrl()). Passing a 
ContentLength is not supported by the SDK, since it is not enforced on 
S3's side given the way the SDK is currently generating these URLs. 
See GitHub issue #457."

我发现在某些场合,必须包含ContentLength(特别是如果你的客户端通过它以便签名匹配),那么在其他场合,如果你包含ContentLength参数错误,getSignedUrl会抱怨:&#34; contentlength预约网址不支持&#34;。我注意到当我更改正在进行呼叫的机器时,行为会发生变化。据推测,另一台机器与该场中的另一台亚马逊服务器建立了连接。

我只能猜测为什么行为在某些情况下存在,而在其他情况下则不存在。也许并非所有亚马逊的服务器都已完全升级?在任何一种情况下,为了处理这个问题,我现在尝试使用ContentLength,如果它给我参数错误,那么我在没有它的情况下再次调用getSignedUrl。这是解决SDK的这种奇怪行为的解决方法。

一个小例子......看起来不是很漂亮,但你明白了:

MediaBucketManager.getPutSignedUrl = function ( params, next ) {
    var _self = this;
    _self._s3.getSignedUrl('putObject', params, function ( error, data ) {
        if (error) {
            console.log("An error occurred retrieving a signed url for putObject", error);
            // TODO: build contextual error
            if (error.code == "UnexpectedParameter" && error.message.search("ContentLength") > -1) {
                if (params.ContentLength) delete params.ContentLength
                MediaBucketManager.getPutSignedUrl(bucket, key, expires, params, function ( error, data ) {
                    if (error) {
                        console.log("An error occurred retrieving a signed url for putObject", error);
                    } else {
                        console.log("Retrieved a signed url for putObject:", data);
                        return next(null, data)
                    }
                }); 
            } else {
                return next(error); 
            }
        } else {
            console.log("Retrieved a signed url for putObject:", data);
            return next(null, data);
        }
    });
};

所以,下面的内容并不完全正确(在某些情况下会更正,但在其他情况下会给你参数错误),但可能会帮助你开始。

旧答案

似乎(对于一个signedUrl将文件PUT输出到只存在公共读取ACL的S3),当向PUT发出请求时,会有几个标头被比较。将它们与传递给getSignedUrl的内容进行比较:

CacheControl: 'STRING_VALUE',
ContentDisposition: 'STRING_VALUE',
ContentEncoding: 'STRING_VALUE',
ContentLanguage: 'STRING_VALUE',
ContentLength: 0,
ContentMD5: 'STRING_VALUE',
ContentType: 'STRING_VALUE',
Expires: new Date || 'Wed De...'

请在此处查看完整列表:http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property

当你打电话给getSignedUrl时,你会传递一个&#39; params&#39;对象(在文档中相当清楚),包括Bucket,Key和Expires数据。这是一个(NodeJS)示例:

var params = { Bucket:bucket, Key:key, Expires:expires };
s3.getSignedUrl('putObject', params, function ( error, data ) {
    if (error) {
        // handle error
    } else {
        // handle data
    }
});

不太清楚的是将ACL设置为&#39; public-read&#39;:

var params = { Bucket:bucket, Key:key, Expires:expires, ACL:'public-read' };

传递标题的概念非常模糊,您期望客户端使用签名的URL将PUT操作传递给S3:

var params = {
    Bucket:bucket,
    Key:key,
    Expires:expires,
    ACL:'public-read',
    ContentType:'image/png',
    ContentLength:7469
};

在上面的示例中,我已经包含了ContentType和ContentLength,因为在javascript中使用XmlHTTPRequest时会包含这两个标头,并且在Content-Length的情况下无法更改。我怀疑像Curl这样的HTTP请求的其他实现会出现这种情况,因为在提交包含正文(数据)的HTTP请求时它们是必需的标头。

如果客户端在请求signedUrl时没有包含有关该文件的ContentType和ContentLength数据,那么当需要将文件PUT到S3(使用该signedUrl)时,S3服务将找到客户端附带的头文件&# 39; s请求(因为它们是必需的标题)但签名不会包含它们 - 因此,它们将不匹配,操作将失败。

因此,在进行getSignedUrl调用之前,您似乎必须知道文件的内容类型和内容长度为PUT到S3。这对我来说不是问题,因为我暴露了一个REST端点,允许我们的客户端在对PUT进行S3操作之前请求签名的URL。由于客户端可以访问要提交的文件(在他们准备提交的时刻),客户端访问文件大小和类型并从我的端点请求带有该数据的签名URL是一项微不足道的操作。 / p>

答案 1 :(得分:5)

根据@Reinsbrain请求,这是实现客户端上传到具有“公共读取”权限的服务器的Node.js版本。

BACKEND(NODE.JS)

var AWS = require('aws-sdk');
var AWS_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY;
var AWS_SECRET_ACCESS_KEY = process.env.S3_SECRET;
AWS.config.update({accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY});
var s3 = new AWS.S3();
var moment = require('moment');
var S3_BUCKET = process.env.S3_BUCKET;
var crypto = require('crypto');
var POLICY_EXPIRATION_TIME = 10;// change to 10 minute expiry time
var S3_DOMAIN = process.env.S3_DOMAIN;

exports.writePolicy = function (filePath, contentType, maxSize, redirect, callback) {
  var readType = "public-read";

  var expiration = moment().add('m', POLICY_EXPIRATION_TIME);//OPTIONAL: only if you don't want a 15 minute expiry

  var s3Policy = {
    "expiration": expiration,
    "conditions": [
      ["starts-with", "$key", filePath],
      {"bucket": S3_BUCKET},
      {"acl": readType},
      ["content-length-range", 2048, maxSize], //min 2kB to maxSize
      {"redirect": redirect},
      ["starts-with", "$Content-Type", contentType]
    ]
  };

  // stringify and encode the policy
  var stringPolicy = JSON.stringify(s3Policy);
  var base64Policy = Buffer(stringPolicy, "utf-8").toString("base64");

  // sign the base64 encoded policy
  var testbuffer = new Buffer(base64Policy, "utf-8");

  var signature = crypto.createHmac("sha1", AWS_SECRET_ACCESS_KEY)
    .update(testbuffer).digest("base64");

  // build the results object to send to calling function
  var credentials = {
    url: S3_DOMAIN,
    key: filePath,
    AWSAccessKeyId: AWS_ACCESS_KEY_ID,
    acl: readType,
    policy: base64Policy,
    signature: signature,
    redirect: redirect,
    content_type: contentType,
    expiration: expiration
  };

  callback(null, credentials);
}

FRONTEND 假设服务器的值在输入字段中,并且您通过表单提交提交图像(即POST,因为我无法让PUT工作):

function dataURItoBlob(dataURI, contentType) {
  var binary = atob(dataURI.split(',')[1]);
  var array = [];
  for(var i = 0; i < binary.length; i++) {
    array.push(binary.charCodeAt(i));
  }
  return new Blob([new Uint8Array(array)], {type: contentType});
}

function submitS3(callback) {
  var base64Data = $("#file").val();//your file to upload e.g. img.toDataURL("image/jpeg")
  var contentType = $("#contentType").val();
  var xmlhttp = new XMLHttpRequest();
  var blobData = dataURItoBlob(base64Data, contentType);

  var fd = new FormData();
  fd.append('key', $("#key").val());
  fd.append('acl', $("#acl").val());
  fd.append('Content-Type', contentType);
  fd.append('AWSAccessKeyId', $("#accessKeyId").val());
  fd.append('policy', $("#policy").val());
  fd.append('signature', $("#signature").val());
  fd.append("redirect", $("#redirect").val());
  fd.append("file", blobData);

  xmlhttp.onreadystatechange=function(){
    if (xmlhttp.readyState==4) {
      //do whatever you want on completion
      callback();
    }
  }
  var someBucket = "your_bucket_name"
  var S3_DOMAIN = "https://"+someBucket+".s3.amazonaws.com/";
  xmlhttp.open('POST', S3_DOMAIN, true);
  xmlhttp.send(fd);
}

注意:我每次提交时都上传了超过1张图片,因此我添加了多个iframe(上面带有FRONTEND代码)来同时上传多张图片。

答案 2 :(得分:3)

第1步:设置s3政策:

{
    "expiration": "2040-01-01T00:00:00Z",
    "conditions": [
                    {"bucket": "S3_BUCKET_NAME"},
                    ["starts-with","$key",""],
                    {"acl": "public-read"},
                    ["starts-with","$Content-Type",""],
                    ["content-length-range",0,524288000]
                  ]
}

第2步:准备aws密钥,策略,签名,在本例中,所有存储在s3_tokens字典中

这里的诀窍在于政策&amp;签名 政策: 1)在文件中保存第1步策略。将其转储到json文件。 2)base 64编码的json文件(s3_policy_json):

#python
policy = base64.b64encode(s3_policy_json)

签名:

#python
s3_tokens_dict['signature'] = base64.b64encode(hmac.new(AWS_SECRET_ACCESS_KEY, policy, hashlib.sha1).digest())

第3步:来自你的js

$scope.upload_file = function(file_to_upload,is_video) {
    var file = file_to_upload;
    var key = $scope.get_file_key(file.name,is_video);
    var filepath = null;
    if ($scope.s3_tokens['use_s3'] == 1){
       var fd = new FormData();
       fd.append('key', key);
       fd.append('acl', 'public-read'); 
       fd.append('Content-Type', file.type);      
       fd.append('AWSAccessKeyId', $scope.s3_tokens['aws_key_id']);
       fd.append('policy', $scope.s3_tokens['policy']);
       fd.append('signature',$scope.s3_tokens['signature']);
       fd.append("file",file);
       var xhr = new XMLHttpRequest();
       var target_url = 'http://s3.amazonaws.com/<bucket>/';
       target_url = target_url.replace('<bucket>',$scope.s3_tokens['bucket_name']);
       xhr.open('POST', target_url, false); //MUST BE LAST LINE BEFORE YOU SEND 
       var res = xhr.send(fd);
       filepath = target_url.concat(key);
    }
    return filepath;
};

答案 3 :(得分:1)

您实际上可以使用上面指定的getSignedURL。以下是如何获取从S3读取的URL,以及使用getSignedURL发布到S3的示例。上传的文件具有与用于生成URL的IAM用户相同的权限。您注意到的问题可能与您如何使用curl进行测试有关?我使用AFNetworking(AFHTTPSessionManager uploadTaskWithRequest)从我的iOS应用程序上传。以下是有关如何使用签名网址发布的示例:http://pulkitgoyal.in/uploading-objects-amazon-s3-pre-signed-urls/

var s3 = new AWS.S3();  // Assumes you have your credentials and region loaded correctly.

这是从S3读取的。 URL将工作60秒。

var params = {Bucket: 'mys3bucket', Key: 'file for temp access.jpg', Expires: 60};
var url = s3.getSignedUrl('getObject', params, function (err, url) {
          if (url) console.log("The URL is", url);
       });

这是写入S3的。 URL将工作60秒。

        var key = "file to give temp permission to write.jpg";
        var params = {
            Bucket: 'yours3bucket',
            Key: key,
            ContentType: mime.lookup(key),      // This uses the Node mime library
            Body: '',
            ACL: 'private',
            Expires: 60
        };
        var surl = s3.getSignedUrl('putObject', params, function(err, surl) {
            if (!err) {
                console.log("signed url: " + surl);
            } else {
                console.log("Error signing url " + err);
            }
        });

答案 4 :(得分:0)

听起来您并不真正需要签名的网址,只是希望您的上传内容可以公开查看。如果是这种情况,您只需要转到AWS控制台,选择要配置的存储桶,然后单击权限。然后单击“添加存储桶策略”按钮并输入以下规则:

{
    "Version": "2008-10-17",
    "Id": "http referer policy example",
    "Statement": [
        {
            "Sid": "readonly policy",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::BUCKETNAME/*"
        }
    ]
}

其中BUCKETNAME应替换为您自己的存储桶名称。现在任何人都可以读取该存储桶的内容,前提是它们具有指向特定文件的直接链接。

答案 5 :(得分:0)

您是否可以使用您的PUT预签名网址上传而无需担心权限,但是立即使用GET方法创建另一个预签名网址并且无限期到期,并向观众提供

答案 6 :(得分:-1)

您使用的是AWS Node.js官方SDK吗? http://aws.amazon.com/sdkfornodejs/

以下是我使用它的方式......

 var data = {
        Bucket: "bucket-xyz",
        Key: "uploads/" + filename,
        Body: buffer,
        ACL: "public-read",
        ContentType: mime.lookup(filename)
    };
 s3.putObject(data, callback);

我上传的文件是公共可读的。希望它有所帮助。