Получение 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

person Glenn    schedule 22.10.2017    source источник


Ответы (8)


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

var options = { headers: { 'Content-Type': fileType, 'x-amz-acl': 'public-read' } };
person Michael - sqlbot    schedule 22.10.2017
comment
Спасибо, это было не так, но я думаю, что я лучше понимаю, в чем проблема. Я почти уверен, что что-то не совпадает, и я просто не понимаю, что именно. - person Glenn; 22.10.2017
comment
Однако это должно быть частью этого. Это очень детерминированный процесс с нулевой терпимостью к несогласованности. CanonicalRequest в сообщении об ошибке косвенно скажет вам, какие параметры вы должны передать getSignedUrl() на основе фактически отправляемого вами запроса. - person Michael - sqlbot; 22.10.2017
comment
Спасибо, наконец-то добавили правильные заголовки и успешно отправили запрос на размещение. - person Glenn; 29.10.2017
comment
Какие заголовки вы вставили. Я столкнулся с той же проблемой. - person Arunkumar Papena; 21.10.2018
comment
@ Алан, не могли бы вы рассказать, как вы решили проблему? Таким образом, другие люди с такой же проблемой могли бы найти решение, спасибо. - person Carlos Salazar; 29.04.2019

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

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

На основе этого ответа https://stackoverflow.com/a/53542531/2759427

person Coburn    schedule 30.09.2019

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

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

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

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

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

  ContentType: req.body.fileType,
  ACL: 'public-read'
person John Hanley    schedule 22.10.2017
comment
Спасибо, я просмотрел этот список и полагаю, что, возможно, есть несоответствие заголовка. Как сравнить заголовки в подписанном URL-адресе с заголовками, которые я отправляю? - person Glenn; 22.10.2017
comment
Вы указываете заголовки HTTP в своих параметрах при вызове getSignedUrl. Также обратите внимание на ответ Майкла sqlbot с заголовком x-amz-acl. Параметры Майкла верны. Убедитесь, что вы включили те же заголовки в свой PUT. - person John Hanley; 22.10.2017
comment
Еще один предмет. Добавьте Content-Length с размером файла при подписании и вызове put. Я не вижу, чтобы регион был указан. Добавьте это при создании клиента. - person John Hanley; 22.10.2017

Была такая же проблема, вот как ее решить:

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

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

РЕДАКТИРОВАТЬ1: ваша подпись также будет недействительной, если вы передадите неправильный тип содержимого.

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

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

person Kannaiyan    schedule 22.10.2017
comment
Спасибо, я попробовал, но не думаю, что это проблема. URL-адрес уже выглядит правильно закодированным, и если я попытаюсь перекодировать его, ссылка фактически станет неработающей. - person Glenn; 22.10.2017
comment
Не могли бы вы подтвердить, что ваш тип контента такой же, как и у axios, а затем вы создали свой подписанный URL? - person Kannaiyan; 22.10.2017

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

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

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

В любом случае, если вы хотите поддерживать загрузку любого типа файла, вероятно, лучше всего передать тип содержимого файла в конечную точку 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()
    }
})

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

person Roman Scher    schedule 13.10.2019
comment
Спасибо тебе за это. И PAW, и JS fetch добавляли типы содержимого по умолчанию в мои заголовки - я не думал проверять! - person redPanda; 01.07.2021

Этот код работал с учетными данными и корзиной, которую я создал несколько лет назад, но вызывал ошибку 403 для недавно созданных учетных данных / корзин:

const s3 = new AWS.S3({
  region: region,
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
})

Исправление заключалось в том, чтобы просто добавить signatureVersion: 'v4'.

const s3 = new AWS.S3({
  signatureVersion: 'v4',
  region: region,
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
})

Почему? Я не знаю.

person pa1nd    schedule 04.09.2020

Как указывали другие, решение состоит в том, чтобы добавить signatureVerision.

const s3 = new AWS.S3(
  {
    apiVersion: '2006-03-01',
      signatureVersion: 'v4'
  }
);

Там очень подробное обсуждение того же взгляда https://github.com/aws/aws-sdk-js/issues/468

person rbansal    schedule 11.06.2021

TL; DR: убедитесь, что ваша корзина существует и доступна с помощью ключа AWS, который генерирует подписанный URL.

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

Поскольку сервер не выдавал никаких ошибок, я предположил, что проблема должна быть вызвана загрузкой, не понимая, что у моего локального сервера было старое имя сегмента в его .env-файле, которое раньше было правильным, но имеет с тех пор был перемещен.

Боковое примечание: эта ссылка помогла https://aws.amazon.com/premiumsupport/knowledge-center/s3-troubleshoot-403/

Именно при проверке политик IAM загружающих пользователей я обнаружил, что у пользователя был доступ к нескольким корзинам, но в настоящее время существует только одна из них.

person jrose    schedule 15.07.2021