Как загрузить клиентский файл на Amazon S3 на стороне клиента?
Позвольте мне начать с того, что я обычно очень неохотно публикую эти вопросы, поскольку я всегда чувствую, что есть ответ на все, что есть в Интернете. Проведя бесчисленные часы в поисках ответа на этот вопрос, я, наконец, отказался от этого утверждения.
Предположение
Это работает:
s3.getSignedUrl('putObject', params);
Что я пытаюсь сделать?
- Загрузите файл через PUT (с клиентской стороны) в Amazon S3, используя метод getSignedUrl
- Разрешить всем просматривать файл, загруженный на S3
Примечание.. Если есть более простой способ разрешить загрузке на стороне клиента (iPhone) на Amazon S3 с предварительно подписанными URL-адресами (и без предоставления учетных данных на стороне клиента), я все уши.
Основные проблемы *
- При просмотре Консоли управления AWS загруженный файл имеет пустые разрешения и метаданные.
- При просмотре загруженного файла (т.е. дважды щелкнув файл в AWS Management Console) я получаю ошибку
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: добавление Content-Type и ACL в PUT
Я также попытался добавить Content-Type и x-amz-acl в свой код, заменив параметры следующим образом:
var params = {Bucket: mybucket, Key: "test.jpg", Expires: 600, ACL: "public-read-write", ContentType: "image/jpeg"};
Затем я попробую хороший ol '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>
И... Теперь я здесь.
Ответы
Ответ 1
Обновление
У меня плохие новости. Согласно примечаниям к выпуску SDK 2.1.6 на 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 side given the way the SDK is currently generating these URLs.
See GitHub issue #457."
Я обнаружил, что в некоторых случаях ContentLength должен быть включен (особенно если ваш клиент передает его так, чтобы подписи соответствовали), а затем в других случаях getSignedUrl будет жаловаться, если вы включите ContentLength с ошибкой параметра: "contentlength не поддерживается в назначенных URL-адресах". Я заметил, что поведение изменится, когда я сменил машину, которая делала вызов. Предположительно, другая машина подключилась к другому серверу Amazon на ферме.
Я могу только догадываться, почему поведение существует в некоторых случаях, но не в других. Возможно, не все серверы Amazon были полностью обновлены? В любом случае, чтобы справиться с этой проблемой, теперь я пытаюсь использовать 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);
}
});
};
Итак, ниже не совсем правильно (в некоторых случаях это будет правильно, но даст вам ошибку параметра в других), но может помочь вам приступить к работе.
Старый ответ
Кажется, что для подписанного Url PUT файл для S3, где есть только ACL с открытым доступом) есть несколько заголовков, которые будут сравниваться, когда запрос будет сделан для 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, вы передадите объект "params" (достаточно понятный в документации), который включает данные 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 для "общедоступного чтения":
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, потому что эти два заголовка включены при использовании XmlHTTPRequest в javascript, а в случае Content-Length не могут быть изменены. Я подозреваю, что это будет иметь место для других реализаций HTTP-запросов, таких как Curl, и таких, потому что они необходимы заголовкам при отправке HTTP-запросов, содержащих тело (данных).
Если клиент не включает данные ContentType и ContentLength о файле при запросе имени signedUrl, когда приходит время, чтобы ОТКЛЮЧИТЬ файл на S3 (с этим подписаннымUrl), служба S3 найдет заголовки, включенные в клиентские запросы (потому что они требуются заголовки), но подпись не включила их, и поэтому они не будут совпадать, и операция завершится неудачно.
Итак, похоже, вам нужно будет знать, до того, как вы вызове getSignedUrl, тип контента и длина содержимого файла будут PUT до S3. Для меня это не проблема, потому что я выставил конечную точку REST, чтобы наши клиенты могли запросить подписанный URL-адрес непосредственно перед тем, как сделать операцию PUT на S3. Поскольку клиент имеет доступ к файлу, который будет отправлен (на момент, когда он готов к отправке), для клиента было тривиальной операцией получить доступ к размеру и типу файла и запросить подписанный URL-адрес с этими данными с моей конечной точки.
Ответ 2
В соответствии с запросом @Reinsbrain это версия Node.js для реализации клиентской загрузки на сервер с правами "public-read".
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);
}
Примечание. Я загружал более одного изображения для каждого представления, поэтому добавлял несколько iframe (с кодом FRONTEND выше) для одновременной загрузки нескольких изображений.
Ответ 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
трюк здесь в политике и подписи
Политика:
1) сохранить шаг 1 в файле. выгрузите его в json файл.
2) базовый 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;
};
Ответ 4
Фактически вы можете использовать getSignedURL, как указано выше. Здесь приведен пример того, как получить URL-адрес для чтения с S3, а также использовать getSignedURL для отправки на S3. Файлы загружаются с теми же правами, что и пользователь IAM, который использовался для создания URL-адресов. Проблемы, которые вы заметили, могут быть функцией того, как вы тестируете с помощью завитки? Я загрузил приложение iOS из приложения AFNetworking (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);
}
});
Ответ 5
Похоже, вам действительно не нужен подписанный URL-адрес, просто чтобы вы могли публично просматривать ваши загрузки. В этом случае вам просто нужно перейти на консоль 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 следует заменить собственным именем ведра. Содержимое этого ведра будет доступно для чтения кому угодно, если у них есть прямая ссылка на конкретный файл.
Ответ 6
Не могли бы вы просто загрузить с помощью своего предварительно подписанного URL-адреса PUT, не беспокоясь о разрешениях, но сразу же создать еще один предварительно подписанный URL-адрес с методом GET и бесконечным истечением срока и предоставить это публике?
Ответ 7
Используете ли вы официальный 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);
И мои загруженные файлы общедоступны. Надеюсь, что это поможет.