这确实有点令人困惑,我认为这是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维护人员自己完成。