Получение 403 (Запрещено) при загрузке на S3 с подписанным URL-адресом

Я пытаюсь создать предварительно подписанный URL-адрес, а затем загружать файл на S3 через браузер. Мой серверный код выглядит так, и он генерирует URL-адрес:

let s3 = new aws.S3({
  // for dev purposes
  accessKeyId: 'MY-ACCESS-KEY-ID',
  secretAccessKey: 'MY-SECRET-ACCESS-KEY'
});
let params = {
  Bucket: 'reqlist-user-storage',
  Key: req.body.fileName, 
  Expires: 60,
  ContentType: req.body.fileType,
  ACL: 'public-read'
};
s3.getSignedUrl('putObject', params, (err, url) => {
  if (err) return console.log(err);
  res.json({ url: url });
});

Кажется, эта часть работает нормально. Я могу видеть URL-адрес, если я его запишу, и он передает его в интерфейс. Затем, на переднем конце, я пытаюсь загрузить файл с аксиомами и подписанным URL:

.then(res => {
    var options = { headers: { 'Content-Type': fileType } };
    return axios.put(res.data.url, fileFromFileInput, options);
  }).then(res => {
    console.log(res);
  }).catch(err => {
    console.log(err);
  });
}

При этом я получаю ошибку 403 Forbidden. Если я следую ссылке, там есть XML с дополнительной информацией:

<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your key and signing method.
</Message>
...etc

Ответ 1

Ваш запрос должен точно соответствовать подписке. Одна очевидная проблема заключается в том, что вы фактически не включаете консервированный ACL в запрос, даже если вы включили его в подпись. Перейдите к следующему:

var options = { headers: { 'Content-Type': fileType, 'x-amz-acl': 'public-read' } };

Ответ 2

Если бы та же проблема, вот как вам нужно ее решить,

  1. Извлеките часть имени файла подписанного URL. Сделайте распечатку, которая правильно извлекает часть имени файла с параметрами querystring. Это важно.
  2. Кодировать в URI Кодирование имени файла с параметрами строки запроса.
  3. Верните URL-адрес из вашей лямбда с зашифрованным именем файла вместе с другим путем или с вашего узла.

Теперь сообщение из аксиомов с этим URL-адресом будет работать.

EDIT1: Ваша подпись также будет недействительной, если вы передадите неправильный тип контента.

Убедитесь, что тип контента, который у вас есть, содержит заранее подписанный URL-адрес, такой же, как тот, который вы используете для ввода.

Надеюсь, поможет.

Ответ 3

1) Возможно, вам придется использовать сигнатуры S3V4 в зависимости от того, как данные передаются в AWS (chunk versus stream). Создайте клиент следующим образом:

var s3 = new AWS.S3({
  signatureVersion: 'v4'
});

2) Не добавляйте новые заголовки или не изменяйте существующие заголовки. Запрос должен быть точно подписан.

3) Убедитесь, что полученный URL соответствует тому, что отправляется в AWS.

4) Сделайте тестовый запрос, удалив эти две строки перед подписанием (и удалите заголовки из PUT). Это поможет сузить проблему:

  ContentType: req.body.fileType,
  ACL: 'public-read'

Ответ 4

Если вы пытаетесь использовать ACL, убедитесь, что у вашей роли Lambda IAM есть s3:PutObjectAcl для данного Bucket, а также что в вашем баке есть s3:PutObjectAcl для загрузки Принципала (user/iam/account, который загружает).

Это то, что я исправил после двойной проверки всех моих заголовков и всего остального.

Вдохновленный этим ответом fooobar.com/questions/17874160/...

Ответ 5

Получение ошибки 403 Forbidden для предварительно подписанной загрузки s3 put также может произойти по нескольким причинам, которые не сразу очевидны:

  1. Это может произойти, если вы сгенерировали предварительно подписанный URL-адрес пут с использованием типа контента подстановочного знака, такого как image/*, поскольку подстановочные знаки не поддерживаются.

  2. Это может произойти, если вы сгенерировали предварительно подписанный URL-адрес с типом содержимого не указан, но затем передали заголовок типа содержимого при загрузке из браузера. Если вы не указали тип контента при генерации URL-адреса, вы должны пропустить тип контента при загрузке. Имейте в виду, что если вы используете инструмент загрузки, например Uppy, он может автоматически прикреплять заголовок типа контента, даже если вы его не указали. В этом случае вам придется вручную установить пустой заголовок типа контента.

В любом случае, если вы хотите поддерживать загрузку файлов любого типа, лучше всего передать тип содержимого файла конечной точке API и использовать этот тип содержимого при создании предварительно подписанного URL-адреса, который вы возвращаете своему клиенту.

Например, создание предварительно подписанного URL из вашего API:

const AWS = require('aws-sdk')
const uuid = require('uuid/v4')

async function getSignedUrl(contentType) {
    const s3 = new AWS.S3({
        accessKeyId: process.env.AWS_KEY,
        secretAccessKey: process.env.AWS_SECRET_KEY
    })
    const signedUrl = await s3.getSignedUrlPromise('putObject', {
        Bucket: 'mybucket',
        Key: 'uploads/${uuid()}',
        ContentType: contentType
    })

    return signedUrl
}

А затем отправляем запрос на загрузку из браузера:

import Uppy from '@uppy/core'
import AwsS3 from '@uppy/aws-s3'

this.uppy = Uppy({
    restrictions: {
        allowedFileTypes: ['image/*'],
        maxFileSize: 5242880, // 5 Megabytes
        maxNumberOfFiles: 5
    }
}).use(AwsS3, {
    getUploadParameters(file) {
        async function _getUploadParameters() {
            let signedUrl = await getSignedUrl(file.type)
            return {
                method: 'PUT',
                url: signedUrl
            }
        }

        return _getUploadParameters()
    }
})

Для получения дополнительной информации см. также следующие два сообщения о переполнении стека: как-генерировать-aws-s3-pre-подписанный-url-запрос-без-знания-типа-контента и S3.getSignedUrl для приема нескольких типов контента