Подтвердить что ты не робот

Как загрузить клиентский файл на 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>

И... Теперь я здесь.

4b9b3361

Ответ 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);

И мои загруженные файлы общедоступны. Надеюсь, что это поможет.