纯 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"
        }
    }
}
二、计算签名
官方文档
文档中对 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"。
- 也就是说,我们需要计算或拼接 Content-MD5( --> 获取文件的 Content-MD5)、Date( --> 获取当前 GMT 时间的 Python 实现)、CanonicalizedOSSHeaders、CanonicalizedResource。
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
三、请求头构造
分析
请求头一些关键必要的:Authorization、Date、Content-MD5、Content-Type、x-oss-callback-var、x-oss-callback、x-oss-security-token。
其中 Authorization、Date、Content-MD5、Content-Type 在第一步骤中已经获取到了。而 x-oss-callback-var、x-oss-callback、x-oss-security-token 是我们需要生成的。
x-oss-callback-var、x-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)
# 接下来判断上传结果
- 注意:这里应该使用 raw 上传文件。-> python-上传文件的几种方式 - 南方的墙 - 博客园
- 获取最后的图片地址:我这里的情况的话,图片地址为社区指定的图片服务器域名(应该解析到了阿里云 OSS),后续即为第一步中的 uploadFileName。- 社区服务器上检验了图片域名,如果不这样的话,将会发帖失败,以供参考。
 
最后大功告成!美滋滋

 
    
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号