使用签名 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(禁止)的主要内容,如果未能解决你的问题,请参考以下文章