S3 预签名 URL 在存储桶创建后 90 分钟有效

pml*_*mlk 6 python amazon-s3 boto3

我们生成预签名 URL,以便用户将文件直接上传到 S3 存储桶。运行集成测试时,我们发现了一个失败的测试,其中预签名 URL 上的 HTTP PUT 请求产生了SignatureDoesNotMatch错误响应。令人惊讶的是,相同的代码使用另一个存储桶可以正常工作。我们一直在导致测试失败的原始存储桶上进行尝试,当它突然开始工作而没有任何实际代码更改时,我们感到很惊讶。

我们注意到,测试成功通过时,大约是在我们创建存储桶后两个小时。由于我们位于 UTC+0200,我们怀疑该问题与该时差和/或某些时钟同步问题有关。我们开始证实我们的怀疑,即经过足够的时间后,相同的预签名 URL 会突然起作用。剧透:确实如此!

以下代码创建一个全新的存储桶,生成适合文件上传的预签名 URL ( ClientMethod='put_object'),并尝试使用该requests库通过 HTTP PUT 某些数据。我们每 60 秒重试一次 PUT 数据,直到创建存储桶后 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)
Run Code Online (Sandbox Code Playgroud)

我们使用 AWS EKS 集群运行集成测试,在其中创建一个集群以及一些数据库、S3 存储桶等,并在测试完成后拆除所有内容。必须等待 90 分钟才能使 URL 预签名生效是不可行的。

我的问题
我做错了什么吗?
这是预期的行为吗?有可接受的解决方法吗?
有人可以使用上面的代码确认此行为吗?

编辑
我更新了代码以按照评论中的“Michael - sqlbot”的建议在“us-east-1”区域中创建一个存储桶。正如此处记录的那样,奇怪的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&amp;X-Amz-Credential=REDACTED%2F20190609%2Feu-central-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20190609T170351Z&amp;X-Amz-Expires=14400&amp;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>
Run Code Online (Sandbox Code Playgroud)

Mic*_*bot 10

这是您遇到的情况:

临时重定向是一种错误响应,它向请求者发出信号,指示他们应该将请求重新发送到不同的端点。由于 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 区域的有效端点,因此可以从区域字符串构造它们。

s3_client = boto3.client('s3', region_name=region, endpoint_url=('https://s3.' + region + '.amazonaws.com'), config=...)
Run Code Online (Sandbox Code Playgroud)

S3 的长期用户可能会对所有地区都支持这一说法表示怀疑,但截至撰写本文时该说法是准确的。最初,某些区域以前使用破折号而不是点,例如,s3-us-west-2.amazonaws.com这在那些较旧的区域中仍然有效,但现在所有区域都支持上述规范形式。