夜阑卧听风吹雨

铁马冰河入梦来

Loading

纯 Python 代码实现上传阿里云 OSS

背景

首先,我要实现在某社区上发帖,该社区的图片服务器使用到了阿里云的 OSS,因此通过抓包分析和查阅阿里云文档,自己用 Python 实现图片上传。也就是说我并不是很正式的使用 OSS,毕竟阿里云也有自己的 SDK,这里仅是使用临时权限上传到别人的服务器上。

  • 当然本文的签名生成也是可以正确使用的。

文章是几个月后补写的,应该没有什么大问题。我现在还是正常使用的。

一、上传准备

向社区服务器发起请求,社区服务器上再在阿里云上创建临时权限,下发密钥等。

  • 因为不同的服务有不同的请求方式,所以这里略过。仅说明一下据笔者估计,可能会得到的一些通用的东西。
  • 注意:文章后续包含的 self.upload_info 都是这里获取到的数据。

这一步可以获取到例如以下的数据(已经过处理):

{
    "data": {
        "fileInfo": [{
                "name": "aa1236c04949d3e83904a4c705a40dad.jpeg",
                "md5": "123e5ddb72f59c123549ec4c970e8123",
                "uploadFileName": "picture/2021/0114/12/1234588_12345ada_9069_0365@1892x4096.jpeg"
            }
        ],
        "uploadInfo": {
            "accessKeySecret": "BPaackBZ6cvdaGoa6a1MaPtXv81iqKNSu7Ro9MYHVF8k",
            "accessKeyId": "STS.NTZnWEZjYQeaHF3gkocGypGnW",
            "securityToken": "CAIS6gJ1q6Ft5B2yfSjIr5fPJe3xl7V45ercilang2s6bahVv4LFtTz2IH1Nf3hpBu8ftfUxm21X5vYblq4pF8AbFRyUNTz6cgXQt1HPWZHIaaDox2Zt6vT8a6z6aXPS2MvVfJ+2Lrf0ceusbFbpjzJ6xaCAGxypQ12iN+/u6/tgdc9FZhSkSjBECdxKXBaaavUXLnzML/2gHwf3i27Ldipercilanl05aaJoPmV4QGMi0bhmK1H5db6JZ+gZtFveZBkSJK02eV5e+3Z0SvM9gJHs/0u1vUDoW6e4pbEWR4Lv0rYY7qExYE3JgIkaqQwLPaa8qnLzaYm58SKx9Wt20oVbL8TUTzSS6LYmZCbRbPzZotlLueiYyqQiOriaael71kWBlsALx5PdtYbLXt9NAchUDmyKNX8pQmRM178FPHegf9pjMUlnwzyjtOOJkmSRbKCyjofOZI6YE4uOgQfwWv7aKgCfhzY/r3SnzdFJxqAAZDFAC9suvMP2bwttEv7QAIp2+3C21QweFVehAABoUjsmLbRkM7Ki4AjGyjOY1FHZgXap+3d5/0roeuaVIFs2G0tFVgTNU4y+zxfYqDR7e80SEP7+LTjxtCybGeXkw+XZYWZAlD4vo6cJzp6pBNCQW0eNt9k3qpwnFfylorgXHxh",
            "expiration": "2021-01-14T04:52:49Z",
            "endPoint": "http://oss-cn-hangzhou.aliyuncs.com",
            "bucket": "test-oss-image",
            "callbackUrl": "https://admin.test.com/callback/OssUploadSuccessCallback?needAttr=0&versionCode=007"
        }
    }
}

二、计算签名

官方文档

在Header中包含签名 - 对象存储 OSS - 阿里云

文档中对 Authorization 的计算方式如下:

Authorization = "OSS " + AccessKeyId + ":" + Signature
Signature = base64(hmac-sha1(AccessKeySecret,
            VERB + "\n"
            + Content-MD5 + "\n" 
            + Content-Type + "\n" 
            + Date + "\n" 
            + CanonicalizedOSSHeaders
            + CanonicalizedResource))

细节分析如下:

  • AccessKeySecret 表示签名所需的密钥。
  • VERB 表示 HTTP 请求的 Method,主要有 PUT、GET、POST、HEAD、DELETE 等。
  • \n 表示换行符。
  • Content-MD5 表示请求内容数据的 MD5 值,对消息内容(不包括头部)计算 MD5 值获得 128 比特位数字,对该数字进行 base64 编码得出。该请求头可用于消息合法性的检查(消息内容是否与发送时一致),例如”eB5eJF1ptWaXm4bijSPyxw==”,也可以为空。详情请参见RFC2616 Content-MD5
  • Content-Type 表示请求内容的类型,例如”application/octet-stream”,也可以为空。
  • Date 表示此次操作的时间,且必须为 GMT 格式,例如”Sun, 22 Nov 2015 08:16:38 GMT”。
  • CanonicalizedOSSHeaders 表示以 x-oss- 为前缀的 HTTP Header 的字典序排列。
  • CanonicalizedResource 表示用户想要访问的 OSS 资源。

这里我们看一下我们需要自行计算哪些东西。

  • AccessKeySecret 是访问某网址后,会提供给我们的(这里应该是该社区服务器向阿里云请求申请到了 OSS 临时权限然后返回给我们的)。
  • VERB:请求方式我这里为 PUT。
  • Content-Type:我这里是上传的 jpg 图片,都是固定的:"image/jpeg"

Cotent_MD5 算法

获取文件的 Content-MD5 的 Python 代码:

def content_encoding(path):
    """
    计算返回文件的 Content-MD5\n
    其实就是计算文件md5时,不将计算所得的二进制转为hex编码,而是进行base64编码\n
    :param path:文件的路径
    :return:
    """
    h_md5 = hashlib.md5()
    with open(path, 'rb') as f:
        while True:
            data = f.read(4096)
            if not data:
                break
            h_md5.update(data)
    content_base64 = base64.b64encode(h_md5.digest())
    return content_base64.decode('utf-8')

获取当前 GMT 时间

获取当前 GMT 时间 Python 代码:

def get_GMT_time():
    GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
    return datetime.utcnow().strftime(GMT_FORMAT)

CanonicalizedOSSHeaders 拼接

以下抠自官方 Python SDK:

def get_OSS_headers(headers):
    canon_headers = []
    for k, v in headers.items():
        lower_key = k.lower()
        if lower_key.startswith('x-oss-'):
            canon_headers.append((lower_key, v))

    canon_headers.sort(key=lambda x: x[0])

    if canon_headers:
        return '\n'.join(k + ':' + v for k, v in canon_headers) + '\n'
    else:
        return ''

CanonicalizedResource

这里,以我要上传图片为例:

from urllib.parse import unquote
resource = unquote(f"/{self.upload_info['bucket']}/{pic['uploadFileName']}")
# 第一个是要上传的bucket,比如:test-oss-image
# 第二个是要上传的图片名称,可获得形如以下的结果:
# "/test-oss-image/picture/2021/0114/12/2326888_441a5ddb_9069_0365@1892x4096.jpeg"
  • 注意这里不能有 url 编码(也就是说如果文件名有 url 编码,需要解码一下)。
    • 因为我这里要上传的图片名称是请求社区服务器后,服务器返回的数据规定的,含有 url 编码。

计算签名

以下节选自我的代码,请结合注释自行分析所需的代码:

def _get_oss_signature(self, headers, resource):
    headers_str = get_OSS_headers(headers) # 获取CanonicalizedOSSHeaders

    key = self.upload_info['accessKeySecret']
    content_md5 = headers['Content-MD5'] # 计算过的 Content-MD5
    content_type = headers['Content-Type'] # 上传的资源类型
    date = headers['Date'] # 当前的GMT时间
    raw_str = f"PUT\n{content_md5}\n{content_type}\n{date}\n{headers_str}"
    # resource为拼接好的CanonicalizedResource,如:"/test-oss-image/picture/2021/0114/12/2326888_441a5ddb_9069_0365%401892x4096.jpeg"
    raw_str = raw_str + unquote(resource)
    # print(raw_str)
    signature = base64.b64encode(hmac_sha1(key, raw_str)).decode('utf-8')
    # print("签名为:", signature)
    return signature

三、请求头构造

分析

请求头一些关键必要的:AuthorizationDateContent-MD5Content-Typex-oss-callback-varx-oss-callbackx-oss-security-token
其中 AuthorizationDateContent-MD5Content-Type 在第一步骤中已经获取到了。而 x-oss-callback-varx-oss-callbackx-oss-security-token 是我们需要生成的。


x-oss-callback-varx-oss-callback:从名字上得知应该是社区服务器要使用的回调 url,其中的参数因服务器而异。而 x-oss-security-token 是我们上传图片前向服务器请求可以获取到的临时 token。

  • 一些请求头的作用尚未分析。

请求头生成

以下节选改编自我的代码,仅供参考分析,不同服务器可能有不同的参数,需自行解码抓包数据比对查看:

host_url = self.upload_info['endPoint'].split("//") # 服务器提供的数据

# header信息生成
host = self.upload_info['bucket'] + "." + host_url[1] # 拼接出要上传的host
callback_var = {"x:var1": "false"}  # TODO 未知作用
callback_var_b64 = b64_encode(dumps(callback_var)) # 这里用到的函数定义见后面
callback_host = re.match(r'https?://(\w+\.\w+\.(com|cn))/.*', self.upload_info['callbackUrl'])[1] # 服务器提供的数据,拼接回调url
call_back = {
    "callbackBodyType": "application/json",
    "callbackHost": callback_host,
    "callbackUrl": self.upload_info['callbackUrl'], # 服务器提供的数据
    "callbackBody": "{\"bucket\":${bucket},\"object\":${object}}" # 因服务器而异
}
call_back_b64 = b64_encode(dumps(call_back)) # 编码获得最后的x-oss-callback
print("x-oss-callback:" + call_back_b64)

upload_headers = {
    "x-oss-callback-var": callback_var_b64,
    "User-Agent": "aliyun-sdk-android/2.9.2(Linux/Android 11/Redmi%20K30%20;RKQ1.200826.001)", # 因机型而异
    "Host": host,
    "x-oss-callback": call_back_b64,
    "x-oss-security-token": self.upload_info['securityToken'], # 服务器提供的数据
    "Content-Type": "image/jpeg",
    "Accept-Encoding": "gzip"
}

用到的函数:

def dumps(dic):  
    return json.dumps(dic, separators=(',', ':'))  


def b64_encode(text):  
    return base64.b64encode(text.encode('utf-8')).decode('utf-8')

Authorization

就是拼接一下签名:

# accessKeyId是服务器提供的
authorization = f"OSS {self.upload_info['accessKeyId']}:{signature}"
upload_headers['Authorization'] = authorization

四、发送请求,获取结果

上传我们的图片到该社区的阿里云 OSS:

with open("D:\UserData\Desktop\1.jpg", "rb") as f:  
    upload_result = requests.put(url, data=f.read(), headers=upload_headers)
# 接下来判断上传结果
  • 获取最后的图片地址:我这里的情况的话,图片地址为社区指定的图片服务器域名(应该解析到了阿里云 OSS),后续即为第一步中的 uploadFileName
    • 社区服务器上检验了图片域名,如果不这样的话,将会发帖失败,以供参考。

最后大功告成!美滋滋
屏幕截图

posted @ 2021-02-22 19:29  二次蓝  阅读(892)  评论(0编辑  收藏  举报