S3

S3的预签名URL

Posted by mjTree on July 31, 2024

更新于:2024-07-31 20:00

一、概要

默认情况下,所有 Amazon S3 对象都是私有的,只有对象拥有者才具有访问它们的权限。但对象拥有者可以通过创建预签名 URL 与其他人共享对象。预签名 URL 使用安全凭证来授予下载对象的限时权限。可以在浏览器中输入此 URL,或者程序使用此 URL 来下载对象。生成预签名 URL 时,可以设置过期时间来限制其他人对该资源的读写权限时长。

二、签名算法剖析

下面内容是基于 python 的 botocore(1.12.253) 上介绍说明,Amazon S3 有多个签名版本(SigV2SigV3SigV4Hmac),具体可以通过from botocore.auth import AUTH_TYPE_MAPS查看这个字典变量,内容如下。

AUTH_TYPE_MAPS = {
    # Signature Version 2,是较旧的签名版本,已逐步被弃用,不建议使用
    'v2': SigV2Auth,

    # Signature Version 4,当前推荐使用的签名版本,提供更高的安全性和功能
    'v4': SigV4Auth,
    # Signature Version 4,用于查询参数中的签名
    'v4-query': SigV4QueryAuth,

    # Signature Version 3,不如 v4 常用,适用于某些特定情况
    'v3': SigV3Auth,
    # Signature Version 3,专门用于 HTTPS
    'v3https': SigV3Auth,

    # 早期的 S3 签名版本,主要用于向后兼容
    's3': HmacV1Auth,
    # 早期的 S3 签名版本,用于查询参数中的签名
    's3-query': HmacV1QueryAuth,
    # 用于预签名的 POST 请求
    's3-presign-post': HmacV1PostAuth,
    # S3 签名版本 4,当前推荐使用的版本,提供更高的安全性和区域化支持
    's3v4': S3SigV4Auth,
    # S3 签名版本 4,用于查询参数中的签名
    's3v4-query': S3SigV4QueryAuth,
    # S3 签名版本 4,用于预签名的 POST 请求
    's3v4-presign-post': S3SigV4PostAuth,
}

目前官方是推荐大家使用SigV4版本的签名算法,下面我们通过之前的 S3协议 文章提到的s3fs来生成预签名 URL,以及查看其对应底层算法实现逻辑。

from fs.mountfs import MountFS
from fs_s3fs import S3FS

bucket_name = '***'
s3_server_path = '***'
aws_access_key_id = '******'
aws_secret_access_key = '******'
endpoint_url = '******'

s3_object = S3FS(
    bucket_name,
    dir_path=s3_server_path,
    aws_access_key_id=aws_access_key_id,
    aws_secret_access_key=aws_secret_access_key,
    endpoint_url=endpoint_url,
)
file_path = 'test.md'
hmac_presigned_url = s3_object.geturl(f'/{file_path}')

目前测试的boto3版本会默认使用的是Hmac算法(fs_s3fs版本太老,未提供相应的参数配置),上面的查询会使用s3-query,对应的就是HmacV1QueryAuth。接着上面代码,我们继续。

print('hmac的核心逻辑如下')
expires = '1722413313'
string_to_sign = f'GET\n\n\n{expires}\n/{bucket_name}/{s3_server_path}/{file_path}'
new_hmac = hmac.new(aws_secret_access_key.encode('utf-8'), digestmod=sha1)
new_hmac.update(string_to_sign.encode('utf-8'))
signature = encodebytes(new_hmac.digest()).strip().decode('utf-8')

query_dict = {
    'AWSAccessKeyId': aws_access_key_id,
    'Signature': signature,
    'Expires': expires,
}
mock_presigned_url = presigned_url.split('?', 1)[0] + '?' + percent_encode_sequence(query_dict)

测试SigV4版本的话,可以直接新建 boto3 的客户端对象,可以替换fs_s3fs对象的客户端属性。由于版本太老,这里升级了一下本地环境的 boto3 和 botocore 版本,版本均为 1.33.13,然后继续我们的代码。

import boto3
from botocore.client import Config

signature_version = "s3v4"
s3_client = boto3.client('s3', endpoint_url=endpoint_url, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, config=Config(signature_version=signature_version))
s3_object._tlocal.client = s3_client
s3v4_presigned_url = s3_object.geturl(f'/{file_path}')

签名算法具体逻辑在下面,生成时使用的是s3v4-query,对应S3SigV4QueryAuth。通过修改各种信息组装成一个十六进制的字符串签名,然后写入到AWSRequest对象中,上游在处理传入的 request 对象参数得到预签名 URL。

class S3SigV4QueryAuth(SigV4QueryAuth):
	...

class SigV4QueryAuth(SigV4Auth):
    def _inject_signature_to_request(self, request, signature):
        request.url += '&X-Amz-Signature=%s' % signature

class SigV4Auth(BaseSigner):
    def add_auth(self, request):
        datetime_now = datetime.datetime.utcnow()
        request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
        self._modify_request_before_signing(request)
        canonical_request = self.canonical_request(request)
        string_to_sign = self.string_to_sign(request, canonical_request)
        signature = self.signature(string_to_sign, request)
        self._inject_signature_to_request(request, signature)

    def _modify_request_before_signing(self, request):
        print('处理请求头,删除和添加相关信息')
        ...

    def canonical_request(self, request):
        print('构建一个标准化的请求字符串,转换请求方法为大写、规范化URL路径、生成标准查询字符串、获取请求体的SHA256校验和等并返回')
        ...

    def string_to_sign(self, request, canonical_request):
        print('构造一个用于签名的标准化字符串,结合请求的时间戳、凭证范围和规范请求的SHA256哈希值,以生成符合AWS签名版本4标准的StringToSign')
        sts = ['AWS4-HMAC-SHA256']
        sts.append(request.context['timestamp'])
        sts.append(self.credential_scope(request))
        sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
        return '\n'.join(sts)

    def signature(self, string_to_sign, request):
        print('生成AWS请求的签名,基于哈希计算,基于秘密密钥、时间戳、区域名、服务名及请求信息,生成一个十六进制格式的签名')
        key = self.credentials.secret_key
        k_date = self._sign((f"AWS4{key}").encode(), request.context["timestamp"][0:8])
        k_region = self._sign(k_date, self._region_name)
        k_service = self._sign(k_region, self._service_name)
        k_signing = self._sign(k_service, 'aws4_request')
        return self._sign(k_signing, string_to_sign, hex=True)

三、桶对象的分享策略

在 minio 服务中,桶的Access Policy默认为 Private(Public 或公共读写的权限是绝不能放开的),这是为了桶中的对象安全性考虑。当涉及到分享对象时,就需要用到上面描述的预签名 URL 来完成。预签名 URL 由于和过期时间以及对象键(类似文件路径,对象的唯一标识符)有关,保证了唯一性和安全性。

如果需要共享大量对象时,通过动态生成预签名 URL 会是一件相对繁琐的事情。业务想拼接路径下载文件就不太现实,这个时候可开放桶的权限。如果只是读取的话,在 minio 中设置成 Custom,其他类型存储类似公共读之类的选项,可以直接路径拼接下载到文件。如果不修改桶的权限,就需要自己实现一个适配器来完成,当有请求获取相应文件路径的对象时,作为有权限的一方可以协助提供对象文件流返回。