使用预签名URL将文件上传到S3

18

我整晚都在尝试使用Amazon S3预签名URL上传文件。我是通过Java代码生成预签名URL的。

    AWSCredentials credentials = new BasicAWSCredentials( accessKey, secretKey );
    client = new AmazonS3Client( credentials );
    GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest( bucketName, "myfilename", HttpMethod.PUT);
    request.setExpiration( new Date( System.currentTimeMillis() + (120 * 60 * 1000) ));
    return client.generatePresignedUrl( request ).toString();

我希望您能使用生成的预签名URL,使用curl上传文件。
curl -v -H "content-type:image/jpg" -T mypicture.jpg https://mybucket.s3.amazonaws.com/myfilename?Expires=1334126943&AWSAccessKeyId=<accessKey>&Signature=<generatedSignature>

我原以为这个方法和GET一样,可以在非公共的bucket上使用(这不是预签名的目的吗?)。但是,每次尝试时都会出现“拒绝访问”的错误。最后,我感到沮丧,将bucket的权限更改为允许所有人写入。当然,这时预签名URL就可以使用了。我很快将所有人的权限从bucket中删除。现在,我没有权限删除由我自己通过预签名URL上传到我的bucket中的项目。我现在明白我可能应该在上传的内容上加上x-amz-acl头。我猜想,在我弄清楚之前,我会创建几个无法删除的对象。

这引发了几个问题:

  • 如何使用PUT和生成的预签名URL使用curl进行上传?
  • 如何删除已上传的文件和测试用的bucket?

最终目标是,手机将使用这个预签名URL来上传图片。我正在尝试在curl中运行它作为概念验证。

更新: 我在亚马逊论坛上提出了一个问题。如果那里有答案,我会把它作为这里的答案。


亚马逊论坛像往常一样没有提供任何帮助(我的问题没有任何回应),所以我最终不得不通过上传到我的应用程序,然后让我的应用程序使用Java API将图像发送到S3来解决这个问题。 - digitaljoel
你不使用预签名POST的原因是什么?也许它们会更好地工作,即使只是因为它们可能在SDK等方面得到了更多的测试。我肯定能够使用Amazon的Ruby gem在预签名post中设置内容类型。 - Tom Andersen
他们的文档建议使用PUT来放置内容。尽管如此,我花了一些时间尝试使用POST,但它也没有起作用。我认为问题可能出在设置的内容类型上,也许是特定于Java SDK的。一旦我以任何方式将内容放入其中,我的预签名GET就非常有效。我尝试过SDK 1.3.3和1.3.6。我知道现在已经更新到1.3.8,但发布说明没有指出与此问题相关的任何更改。 - digitaljoel
建议:按照 https://dev59.com/T0nSa4cB1Zd3GeqPPJOT#59295183 中的说明,使用基于头部的签名来使用curl上传您的文件。 - Bernard Hauzeur
2个回答

35

这确实有点令人困惑,我认为这是AWS SDK for Java中的一个bug(如下所示)- 但首先,以下curl命令将以此方式上传您的文件(当然,假设已更新预签名URL):

curl -v -T mypicture.jpg https://mybucket.s3.amazonaws.com/myfilename?Expires=1334126943&AWSAccessKeyId=<accessKey>&Signature=<generatedSignature>

我已经排除了Content type头部,因为它会产生application/octet-stream(或binary/octet-stream)的结果,这显然是不希望出现的;因此,需要进一步挖掘。

背景/分析

Amazon S3的预签名URL用于PUT(以及DELETE和HEAD)请求原则上是可行的,这在相关问题中得到证明(例如,请参见我的回答Upload to s3 with curl using pre-signed URL (getting 403))。

文档中使用以下伪语法来说明查询字符串请求认证方法,以实现Query String Request Authentication Alternative

StringToSign = HTTP-VERB + "\n" +
    Content-MD5 + "\n" +
    Content-Type + "\n" +
    Expires + "\n" +
    CanonicalizedAmzHeaders +
    CanonicalizedResource;    

这个问题包括了 Content-Type 头信息,正如你已经发现的那样,在一些文档中遗漏了这个部分,例如 AWS 团队回应 GetPreSignedURL with PUT request 后,添加这个信息就能使预签名 URL 工作。

使用 AWS SDK for .NET 很容易实现这个功能,因为它提供了方便的方法 GetPreSignedUrlRequest.WithContentType 来实现:

设置此请求的 ContentType 属性。此属性默认为 "binary/octet-stream",但如果您需要其他类型,则可以设置此属性。

因此,将相应的示例使用预签名URL上传对象-AWS SDK for .NET扩展如下,可以生成一个带有内容类型的工作预签名URL,可以按预期使用curl上传(即与您尝试的方式完全相同):
    // ...
    GetPreSignedUrlRequest request = new GetPreSignedUrlRequest();
    // ...
    request.WithContentType("image/jpg");
    // ...

现在,我们希望以类似的方式扩展语义相同的示例使用Java AWS SDK上传预签名URL的对象, 但是(正如您已经发现的那样),没有专门的方法来实现这一点。这可能只是缺少便利方法,最终可以通过addRequestParameter()setResponseHeaders()实现,例如:
  // ...
  request.setExpiration( new Date( System.currentTimeMillis() + (120 * 60 * 1000) ));
  request.addRequestParameter("content-type", "image/jpg");
  return client.generatePresignedUrl( request ).toString();
  // ...

然而,两种方法的文档都提到了其他用途,实际上并不起作用,即使设置了任何内容类型,它们始终产生相同的签名。

进一步调试SDK后发现,两者都提供了一个语义上类似的核心方法来根据上述“伪语法”计算查询字符串认证,对于.NET请参考buildSigningString(),对于Java请参考makeS3CanonicalString()

但是,在Java版本中,将所有有趣的标题添加到列表中,然后对它们进行排序的相应代码实际上从未执行,因为确实没有方法来提供这些标题,这些标题仅适用于DefaultRequest类,而不是用于初始化前者的GeneratePresignedUrlRequest类,后者又被用作计算签名的输入,参见受保护方法createRequest()

有趣的是,.NET和Java中计算查询字符串身份验证的两种方法从调用栈的几乎相反的参数来源组合其输入,这可能暗示了Java中的错误原因,但显然这也可能只是难以解释,即内部架构当然可能有很大差异。

初步结论

有两个方面:

  • 对于设置内容类型来说,AWS Java SDK明显缺少方便的方法,这可能是一个比较罕见但明显的用例,在其他AWS SDK中已经考虑到了 - 鉴于其在AWS相关后端服务中广泛使用,这令人惊讶。
  • 无论如何,似乎在实现查询字符串请求身份验证时存在一些问题,例如与.NET版本的比较 - 再次令人惊讶,鉴于它是核心功能,但仍处于S3模型/命名空间之内,因此可能只适用于上述各自的用例。
总之,解决这个问题的唯一合理方法是更新SDK,因此需要提交一个错误报告。当然,也可以复制/扩展SDK功能以单独处理这种特殊情况(最好以一种允许提交aws-sdk-for-java项目拉取请求的方式),但以兼容性和可维护性的方式正确完成似乎有点棘手,因此最好由SDK维护人员自己完成。

2
从版本1.11.8开始(我还没有深入挖掘差异以查看是否/何时可能更改),GeneratePresignedUrlRequest扩展了AmazonWebServiceRequest,为您所有的自定义标头需求提供了putCustomRequestHeader(String key, String value)方法。这是添加签名算法内部包含的标头的方法。这似乎没有被(很好地?)记录下来。此外,您需要手动包括“x-amz-”...在您的标头名称中,以添加用户定义的元数据到上传的对象中。 - meticoeus
正如我最近所学的那样,ContentType需要与实际执行PUT请求的工具相匹配。在我的情况下,我正在使用Ajax,直到我发现了这个:https://dev59.com/c3E85IYBdhLWcg3wSxkv#2845487 才意识到在PreSignedURL中调用设置内容类型将(自然地)失败。回顾往事总是20/20视力,因为这显然是非常明显的,也不言而喻,但我会感激有人说过! :) - Techmag
JavaScript SDK也有同样的问题。您必须手动提供Content-Type才能使预签名的URL正常工作。 - Arsenii Fomin
在curl命令中,URL应该放在双引号中,因为如果没有它,命令将无法工作,AWS会返回一些错误。 - Beemo

0
我也遇到了这个问题。我们已经在后端跟踪文件上传的时间,所以我们的解决方法是在客户端使用Rails应用程序上传文件后,通过调用copy_from来设置内容类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接