Python 利用 S3 的 Presigned URLs 实现无鉴权上传与下载

S3协议操作对象存储服务,通常是实现上传下载功能。 但是在某些场景下,程序不具备操作权限,或为了安全原因而缩小权限配置,需要实现无鉴权的上传与下载。 这时可以用S3协议的Presigned URLs来实现无鉴权读写操作。

推荐使用 PUT 而不是 POST 来实现上传,因为 PUT 使用起来比较简单。

PUT 上传

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

|

import boto3

def gen_s3_presigned_put(bucket: str, path: str) -> str:

    s3r = boto3.resource(

        ‘s3’,

        endpoint_url=S3_ENDPOINT,

        aws_access_key_id=S3_ACCESS_KEY,

        aws_secret_access_key=S3_SECRET_KEY,

        region_name=S3_REGION,

        config=Config(signature_version=‘s3v4’),

    )

    if not s3r.Bucket(bucket).creation_date:

        s3r.create_bucket(Bucket=bucket)

    return s3r.meta.client.generate_presigned_url(

        ClientMethod=‘put_object’,

        Params={

            ‘Bucket’: bucket,

            ‘Key’: path,

        },

        ExpiresIn=3600,

        HttpMethod=‘PUT’,

    )

url = generate_presigned_put(‘bucket’, ‘remote/path/of/file’)

|

generate_presigned_url的源码可见botocore/signers.py#L245

这里的返回值,就是一个在 1 小时内可以用PUT上传的 URL 字符串。 通过某种方式传递 URL 到无鉴权的服务或客户端,就可以实现上传功能。

S3_*等,就是鉴权信息。生成URL需要鉴权信息,使用时则不用。 上面代码中,还包含一个if判定桶的存在性并自动建立的功能。 它在生产环境基本无用,但是方便调试,可以酌情去掉。

以下是上传示例:

|

1

2

3

4

5

6

7

|

import requests

def upload_with_put(url):

    with open(‘local/path/of/file’, ‘rb’) as file:

        response = requests.put(url, data=file)

        response.raise_for_status()

|

GET 下载

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

|

import boto3

def gen_s3_presigned_get(bucket: str, path: str) -> str:

    s3r = boto3.resource(

        ‘s3’,

        endpoint_url=S3_ENDPOINT,

        aws_access_key_id=S3_ACCESS_KEY,

        aws_secret_access_key=S3_SECRET_KEY,

        region_name=S3_REGION,

        config=Config(signature_version=‘s3v4’),

    )

    if not s3r.Bucket(bucket).creation_date:

        s3r.create_bucket(Bucket=bucket)

    return s3r.meta.client.generate_presigned_url(

        ClientMethod=‘get_object’,

        Params={

            ‘Bucket’: bucket,

            ‘Key’: path,

        },

        ExpiresIn=3600,

        HttpMethod=‘GET’,

    )

url = generate_presigned_get(‘bucket’, ‘remote/path/of/file’)

|

整个实现和PUT非常类似,只是改了ClientMethodHttpMethod

以下是下载示例:

|

1

2

3

4

5

|

size = 2**12  # Use 4 KB memery buffer

with open(path, ‘wb’) as file:

    for chunk in response.iter_content(chunk_size=size):

        file.write(chunk)

|

POST 上传

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

|

import boto3

def gen_s3_presigned_post(bucket: str, path: str) -> str:

    s3r = boto3.resource(

        ‘s3’,

        endpoint_url=S3_ENDPOINT,

        aws_access_key_id=S3_ACCESS_KEY,

        aws_secret_access_key=S3_SECRET_KEY,

        region_name=S3_REGION,

        config=Config(signature_version=‘s3v4’),

    )

    if not s3r.Bucket(bucket).creation_date:

        s3r.create_bucket(Bucket=bucket)

    dict_ = s3r.meta.client.generate_presigned_post(

        Bucket=bucket,

        Key=path,

        ExpiresIn=3600,

    )

    return dict_[‘url’], dict_[‘fields’]

url, fields = generate_presigned_post(‘bucket’, ‘remote/path/of/file’)

|

generate_presigned_post的源码可见botocore/signers.py#L605。 除了URL外,fields是个dict,也是必须要传递的参数。 并且似乎需要传递或约定path,也可能使用通用的file也行。

|

1

2

3

4

5

6

7

8

|

import requests

def upload_with_post(url, fields):

    with open(‘local/path/of/file’, ‘rb’) as file:

        files = {‘file’: (‘remote/path/of/file’, file)}

        response = requests.post(url, data=fields, files=files)

        response.raise_for_status()

|

HTML 网页示例如下,其中也可发现fields中包含的具体内容。:

|

1

2

3

4

5

6

7

8

9

10

|

    

      

    File:


|

总结

同样是上传,用POST就会麻烦一些,因此推荐PUT。 二者的区别,可能在于操作的幂等性,POST是不能重复的,而PUT可以。 (仅语义推断,未实测验证。)

分段上传,用Presigned URLs似乎是不支持的。