我们生成预签名的URL,以便用户将文件直接上传到S3存储桶中。运行集成测试时,我们发现一个失败的测试,其中对预签名URL的HTTP PUT请求产生了SignatureDoesNotMatch
错误响应。令人惊讶的是,同一代码在另一个存储桶中运行良好。我们一直在尝试使测试失败的原始存储桶,并惊讶地发现它在没有任何实际代码更改的情况下突然开始工作。
我们注意到创建测试桶大约需要两个小时,测试才能成功通过。由于我们位于UTC + 0200,我们怀疑该问题与该时差和/或某些时钟同步问题有关。我们开始确认我们的怀疑,即经过足够的时间后,相同的预签名URL会突然生效。 SPOILER:可以!
以下代码创建一个全新的存储桶,生成一个适合文件上传的预签名URL(ClientMethod='put_object'
),并尝试使用requests
库对一些数据进行HTTP PUT。我们每60秒重试一次PUTTING数据,直到创建存储桶后最终成功5419秒(或90分钟)。
注意:即使随后删除了存储桶,现在也可以成功运行相同的脚本(使用相同的存储桶名称)。如果您想再次确认此行为,请确保第二次使用其他存储桶名称。
import logging
import time
import boto3
import requests
from botocore.client import Config
logger = logging.getLogger(__name__)
# region = "eu-central-1"
# region = "eu-west-1"
# region = "us-west-1"
region = "us-east-1"
s3_client = boto3.client('s3', region_name=region, config=Config(signature_version='s3v4'))
if __name__ == "__main__":
bucket_name = "some-globally-unique-bucket-name"
key_for_file = "test-file.txt"
# create bucket
if region == "us-east-1":
# https://github.com/boto/boto3/issues/125
s3_client.create_bucket(Bucket=bucket_name, ACL='private')
else:
s3_client.create_bucket(Bucket=bucket_name, ACL='private',
CreateBucketConfiguration={'LocationConstraint': region})
creation_time = time.time()
# generate presigned URL
file_data = b"Hello Test World"
expires_in = 4 * 3600
url = s3_client.generate_presigned_url(ClientMethod='put_object', ExpiresIn=expires_in,
Params={'Bucket': bucket_name, 'Key': key_for_file})
time_since_bucket_creation = time.time() - creation_time
time_interval = 60
max_time_passed = expires_in
success = False
try:
while time_since_bucket_creation < max_time_passed:
response = requests.put(url, data=file_data)
if response.status_code == 200:
success = True
break
if b"<Code>SignatureDoesNotMatch</Code>" in response.content:
reason = "SignatureDoesNotMatch"
else:
reason = str(response.content)
time_since_bucket_creation = time.time() - creation_time
print("="*50)
print(f"{time_since_bucket_creation:.2f} s after bucket creation")
print(f"unable to PUT data to url: {url}")
print(f"reason: {reason}")
print(response.content)
time.sleep(time_interval)
except KeyboardInterrupt:
print("Gracefully shutting down...")
if success:
print("YAY! File Upload was successful!")
time_since_bucket_creation = time.time() - creation_time
print(f"{time_since_bucket_creation:.2f} seconds after bucket creation")
s3_client.delete_object(Bucket=bucket_name, Key=key_for_file)
# delete bucket
s3_client.delete_bucket(Bucket=bucket_name)
我们使用AWS EKS集群运行集成测试,在其中创建集群以及一些数据库,S3存储桶等,并在测试完成后拆除所有组件。必须等待90分钟才能完成URL的预签名工作。
我的问题
我做错什么了吗?
这是预期的行为吗?
有可接受的解决方法吗?
有人可以使用上面的代码确认这种行为吗?
编辑
我更新了代码,以在注释中的“ Michael-sqlbot”建议的“ us-east-1”区域中创建存储桶。如here所述,奇怪的if
语句是必需的。我可以证实迈克尔的怀疑,即“ us-east-1”无法重现该行为。
如果感兴趣,则在错误情况下返回XML:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
<AWSAccessKeyId>REDACTED</AWSAccessKeyId>
<StringToSign>AWS4-HMAC-SHA256
20190609T170351Z
20190609/eu-central-1/s3/aws4_request
c143cb44fa45c56e52b04e61b777ae2206e0aaeed40dafc78e036878fa91dfd6</StringToSign>
<SignatureProvided>REDACTED</SignatureProvided>
<StringToSignBytes>REDACTED</StringToSignBytes>
<CanonicalRequest>PUT
/test-file.txt
X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=REDACTED%2F20190609%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Date=20190609T170351Z&X-Amz-Expires=14400&X-Amz-SignedHeaders=host
host:some-globally-unique-bucket-name.s3.eu-central-1.amazonaws.com
host
UNSIGNED-PAYLOAD</CanonicalRequest>
<CanonicalRequestBytes>REDACTED</CanonicalRequestBytes>
<RequestId>E6CBBC7D2E4D322E</RequestId>
<HostId>j1dM1MNaXaDhzMUXKhqdHd6+/Rl1C3GzdL9YDq0CuP8brQZQV6vbyE9Z63HBHiBWSo+hb6zHKVs=</HostId>
</Error>
答案 0 :(得分:1)
这就是您要碰到的东西:
临时重定向是一种错误响应,向请求者发送信号,告知他们应将请求重新发送到其他端点。由于Amazon S3的分布式特性,可以将请求临时路由到错误的设施。创建或删除存储桶后,很可能会立即发生这种情况。
例如,如果您创建一个新存储桶并立即向该存储桶发出请求,则可能会收到临时重定向,具体取决于存储桶的位置限制。如果您在美国东部(弗吉尼亚北部)AWS地区创建了存储桶,则不会看到重定向,因为这也是默认的Amazon S3终端节点。
但是,如果在其他任何区域中创建了存储桶,则在传播存储桶的DNS条目时,对存储桶的所有请求都将到达默认端点。默认终结点使用HTTP 302响应将请求重定向到正确的终结点。临时重定向包含指向正确工具的URI,您可以使用该URI立即重新发送请求。
https://docs.aws.amazon.com/AmazonS3/latest/dev/Redirects.html
请注意,最后一部分-您可以用来立即重新发送请求的 不太准确。您可以-但是,如果请求使用签名版本4,则重定向到新主机名后将导致SignatureDoesNotMatch
错误,因为主机名将不同。早在签名版本2的早期,存储桶名称已包含在签名中,但端点主机名本身并未包含在内,因此重定向到其他端点主机名不会使签名无效。
如果boto做正确的事情并使用正确的区域终结点创建签名的URL,那么这都不是问题-但由于某种原因,它使用“全局”(通用)终结点-导致S3在存储桶生命周期的前几分钟内发出这些重定向,因为DNS尚未更新,因此请求将错误路由到us-east-1并被重定向。这就是为什么我怀疑us-east-1不会表现出这种行为的原因。
这应该是默认行为,但不是。仍然,似乎应该有一种更干净的方法,可以通过配置自动执行此操作……可能存在……但是我在文档中没有找到它。
作为一种解决方法,客户端构造函数接受一个endpoint_url
参数,这似乎可以达到目的。事实证明,s3.${region}.amazonaws.com
是每个S3区域的valid endpoint,因此可以从区域字符串构造它们。
s3_client = boto3.client('s3', region_name=region, endpoint_url=('https://s3.' + region + '.amazonaws.com'), config=...)
长期使用S3的用户可能会对所有区域都支持这一说法感到怀疑,但是在撰写本文时,它是准确的。最初,某些区域以前使用破折号而不是点,例如s3-us-west-2.amazonaws.com
,这在较早的地区仍然有效,但是现在所有地区都支持上述规范形式。