使用签名 URL 上传到 S3 时出现 403(禁止)

Posted

技术标签:

【中文标题】使用签名 URL 上传到 S3 时出现 403(禁止)【英文标题】:Getting 403 (Forbidden) when uploading to S3 with a signed URL 【发布时间】:2018-04-02 20:33:34 【问题描述】:

我正在尝试生成一个预签名的 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。然后在前端,我正在尝试使用 axios 和签名 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】:

1) 您可能需要使用 S3V4 签名,具体取决于数据传输到 AWS 的方式(块与流)。如下创建客户端:

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

2) 不要添加新标题或修改现有标题。请求必须与签名完全一致。

3) 确保生成的 url 与发送到 AWS 的内容相匹配。

4) 发出测试请求,在签名前删除这两行(并从 PUT 中删除标头)。这将有助于缩小您的问题范围:

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

【讨论】:

谢谢,我浏览了这个列表,我相信可能存在标题不匹配的问题。如何比较已签名 url 中的标头与我发送的标头? 您在调用 getSignedUrl 时在参数中指定 HTTP 标头。另请注意 Michael sqlbot 使用 x-amz-acl 标头的回复。迈克尔的参数是正确的。确保在 PUT 中包含相同的标题。 一个附加项目。在签名和调用 put 时添加 Content-Length 和文件大小。我没有看到指定的区域。创建客户端时添加此项。【参考方案2】:

遇到了同样的问题,下面是你需要解决的方法,

    提取签名 URL 的文件名部分。 打印您正在使用查询字符串参数正确提取文件名部分。这很关键。 使用查询字符串参数将文件名编码为 URI 编码。 从带有编码文件名的 lambda 以及其他路径或节点服务返回 url。

现在使用该网址从 axios 发布,它将起作用。

EDIT1: 如果您传递了错误的内容类型,您的签名也将无效。

请确保您创建预签名 url 的内容类型与您用于 put 的内容类型相同。

希望对你有帮助。

【讨论】:

谢谢,我试了一下,但我不认为这是问题所在。该 URL 似乎已经正确编码,如果我尝试“重新编码”它,它实际上会变成一个死链接。 您能否确认您的内容类型与 axios 相同,然后您已经创建了您的签名网址?【参考方案3】:

您的请求需要与签名完全匹配。一个明显的问题是您实际上并未在请求中包含预设 ACL,即使您将其包含在签名中也是如此。改成这样:

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

【讨论】:

谢谢,不是这样,但我想我对问题所在有了更好的了解。我很确定有些东西不匹配,我只是不知道到底是什么。 不过,这应该是其中的一部分。这是一个非常确定的过程,对不一致的容忍度为零。错误消息中的CanonicalRequest 会间接告诉您,根据您实际发送的请求,您应该将哪些参数传递给getSignedUrl() 谢谢,终于确定了正确的headers并成功提出了put请求。 你放了什么标题。我也面临同样的问题。 @Alan 你能分享一下你是如何解决这个问题的吗?这样其他有相同问题的人可以找到解决方案,谢谢。【参考方案4】:

如果您尝试使用 ACL,请确保您的 Lambda IAM 角色具有给定存储桶的 s3:PutObjectAcl,并且您的存储桶允许上传委托人的 s3:PutObjectAcl(用户/iam/帐户正在上传)。

这是在仔细检查了我的所有标题和其他所有内容后为我解决的问题。

受此答案的启发https://***.com/a/53542531/2759427

【讨论】:

【参考方案5】:

对于预签名的 s3 put 上传接收到 403 Forbidden 错误也可能由于几个并非立即显而易见的原因而发生:

    如果您使用 通配符 内容类型(例如 image/*)生成预签名的 put url,则可能会发生这种情况,因为不支持通配符。

    如果您生成预签名的 put url 未指定内容类型,但在从浏览器上传时传入内容类型标头,则可能会发生这种情况。如果生成 url 时不指定内容类型,则上传时必须省略内容类型。请注意,如果您使用像 Uppy 这样的上传工具,即使您没有指定,它也可能会自动附加内容类型标题。在这种情况下,您必须手动将内容类型标头设置为空。

无论如何,如果您想支持上传任何文件类型,最好将文件的内容类型传递给您的 api 端点,并在生成您返回给客户端的预签名 url 时使用该内容类型。

例如,从您的 api 生成一个预签名的 url:

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-without-knowing-content-type 和 S3.getSignedUrl to accept multiple content-type

【讨论】:

谢谢你。 PAW 和 JS fetch 都将默认内容类型添加到我的标题中 - 我没想到要检查!【参考方案6】:

此代码使用我几年前创建的凭据和存储桶,但在最近创建的凭据/存储桶上导致 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,
)

为什么?我不知道。

【讨论】:

【参考方案7】:

正如其他人指出的那样,解决方案是添加 signatureVersion。

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

周围有很详细的讨论看看https://github.com/aws/aws-sdk-js/issues/468

【讨论】:

【参考方案8】:

TLDR:检查您的存储桶是否存在并且可由生成签名 URL 的 AWS 密钥访问。

所有答案都非常好,很可能是真正的解决方案,但我的问题实际上源于 S3 将签名 URL 返回到不存在的存储桶。

因为服务器没有抛出任何错误,所以我假设它一定是导致问题的上传,而没有意识到我的本地服务器的 .env 文件中有一个旧的存储桶名称曾经是正确的,但后来被搬走了。

旁注:此链接帮助https://aws.amazon.com/premiumsupport/knowledge-center/s3-troubleshoot-403/

在检查上传用户的 IAM 政策时,我发现该用户可以访问多个存储桶,但其中只存在一个。

【讨论】:

【参考方案9】:

在上传文件时使用 python boto3,默认情况下权限是私有的。您可以使用 ACL='public-read' 将对象公开

s3.put_object_acl(
     Bucket='gid-requests', Key='potholes.csv', ACL='public-read')

【讨论】:

【参考方案10】:

我做了这里提到的所有事情,并允许这些权限让它工作:

【讨论】:

以上是关于使用签名 URL 上传到 S3 时出现 403(禁止)的主要内容,如果未能解决你的问题,请参考以下文章

使用预签名 URL 将文件上传到 Amazon S3 时出现 CORS 错误

使用预签名 URL 上传到 S3 存储桶会出现 403 禁止错误

仅从浏览器使用预签名 URL 上传时出现 AWS S3 CORS 错误

为啥从浏览器上传到 S3 时出现 403 错误?

使用预签名的 url PUT 到 S3 会出现 403 错误

PUT 上传文件到 AWS S3 预签名 URL Retrofit2 Android