如何在客户端将可查看file upload到Amazon S3?

让我开始说,我通常很不愿意发表这个问题,因为我总觉得互联网上的所有东西都有答案。 花了无数小时寻找这个问题的答案后,我终于放弃了这个声明。

假设

这工作:

s3.getSignedUrl('putObject', params); 

我想做什么?

  1. 使用getSignedUrl方法通过PUT(从客户端)将file upload到Amazon S3
  2. 允许任何人查看上传到S3的文件

注意:如果有更简单的方法允许客户端(iPhone)通过预先签名的URL上传到Amazon S3(并且不需要公开证书客户端)

主要问题*

  1. 查看AWSpipe理控制台时,上传的文件具有空白的权限和元数据集。
  2. 查看上传的文件时(例如,通过双击AWSpipe理控制台中的文件),我得到一个AccessDenied错误。

我试过了什么?

尝试#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 }); 

预先签名的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上添加内容types和ACL

我也尝试在代码中添加Content-Type和x-amz-acl,像这样replace参数:

 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 

问题
我的terminal输出一些错误:

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

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

尝试#3:修改桶权限是公开的

下面列出的所有项目均在AWSpipe理控制台中打勾)

 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> 

而…现在我在这里。

更新

我有消息。 根据http://aws.amazon.com/releasenotes/1473534964062833上SDK 2.1.6的发行说明:

 "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必须包含(特别是如果你的客户端通过它,签名将匹配),然后在其他场合,getSignedUrl将抱怨,如果你包含ContentLength与参数错误:“contentlength不支持在预先登记的url”。 我注意到,当我改变拨打电话的机器时,行为会改变。 大概另一台机器连接到场中的另一台亚马逊服务器。

我只能猜测为什么行为在某些情况下存在,而不是在其他情况下。 也许不是所有的亚马逊的服务器已经完全升级? 在任何一种情况下,为了解决这个问题,我现在尝试使用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); } }); }; 

所以,下面是不完全正确的(这在某些情况下是正确的,但在别人给你的参数错误),但可能会帮助你开始。

老答案

看起来(对于一个signed Url把一个文件放到只有公共读取ACL的S3上),有一些标题,当对PUT发出一个请求到S3的时候,会被比较。 将它们与传递给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时,你会传递一个包含Bucket,Key和Expires数据的“params”对象(在文档中相当清楚)。 这是一个(NodeJS)的例子:

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

不太清楚的是将ACL设置为“public-read”:

 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数据,那么当S3将该文件放到S3(使用该signedUrl)时,S3服务将查找包含在客户请求中的头(因为它们是必需的标题),但签名不会包含它们 – 所以它们不匹配,操作将失败。

所以,看起来,在进行getSignedUrl调用之前,您必须知道要将文件的内容types和内容长度提交给S3。 这对我来说并不是问题,因为我公开了一个REST端点,允许我们的客户端在对S3执行PUT操作之前请求签名的url。 由于客户端可以访问要提交的文件(在准备提交的时刻),客户端访问文件大小和types并从我的端点请求带有该数据的签名url是一个简单的操作。

根据@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假设来自服务器的值在input字段中,并且您通过表单提交提交图像(即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 eg 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代码)同时进行多图片上传。

步骤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字典中

这里的策略是在策略和签名策略中: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; }; 

实际上,您可以像上面指定的那样使用getSignedURL。 下面是一个例子,说明如何从S3读取URL,还可以使用getSignedURL过帐到S3。 这些文件将以与用于生成URL的IAM用户相同的权限上传。 您注意到的问题可能是您如何testingcurl的function? 我使用AFNetworking从我的iOS应用上传(AFHTTPSessionManager uploadTaskWithRequest)。 以下是如何使用签名url发布的示例: 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); } }); 

这听起来像你并不需要一个签名的URL,只是你希望你的上传是公开的。 如果是这种情况,只需转到AWS控制台,select要configuration的存储桶,然后单击权限即可。 然后点击“添加存储桶策略”button并input以下规则:

 { "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应该replace为你自己的桶名。 这个桶的内容现在可以被任何人读取,只要他们有一个直接链接到一个特定的文件。

您可以使用PUT预先签名的URL上传而不必担心权限,但是立即使用GET方法创build另一个预先签名的URL并且无限期到期,并将其提供给查看公众?

你在使用官方的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); 

而我上传的文件是公共可读的。 希望能帮助到你。