背景 首先,我要实现在某社区上发帖,该社区的图片服务器使用到了阿里云的 OSS,因此通过抓包分析和查阅阿里云文档,自己用 Python 实现图片上传。也就是说我并不是很正式的使用 OSS ,毕竟阿里云也有自己的 SDK ,这里仅是使用临时权限上传到别人的服务器 上。
文章是几个月后补写的,应该没有什么大问题。我现在还是正常使用的。
一、上传准备 向社区服务器发起请求,社区服务器上再在阿里云上创建临时权限,下发密钥等。
因为不同的服务有不同的请求方式,所以这里略过。仅说明一下据笔者估计,可能会得到的一些通用的东西。 注意 :文章后续包含的 self.upload_info
都是这里获取到的数据。这一步可以获取到例如以下的数据(已经过处理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "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 的计算方式如下:
1 2 3 4 5 6 7 8 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 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 代码:
1 2 3 def get_GMT_time (): GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' return datetime.utcnow().strftime(GMT_FORMAT)
以下抠自官方 Python SDK:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 这里,以我要上传图片为例:
1 2 3 4 5 from urllib.parse import unquoteresource = unquote(f"/{self.upload_info['bucket' ]} /{pic['uploadFileName' ]} " )
注意这里不能有 url 编码(也就是说如果文件名有 url 编码,需要解码一下)。因为我这里要上传的图片名称是请求社区服务器后,服务器返回的数据规定的,含有 url 编码。 计算签名 以下节选自我的代码,请结合注释自行分析所需的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def _get_oss_signature (self, headers, resource ): headers_str = get_OSS_headers(headers) key = self .upload_info['accessKeySecret' ] content_md5 = headers['Content-MD5' ] content_type = headers['Content-Type' ] date = headers['Date' ] raw_str = f"PUT\n{content_md5} \n{content_type} \n{date} \n{headers_str} " raw_str = raw_str + unquote(resource) signature = base64.b64encode(hmac_sha1(key, raw_str)).decode('utf-8' ) 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。
请求头生成 以下节选改编自我的代码,仅供参考 分析,不同服务器可能有不同的参数,需自行解码抓包数据 比对查看:
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 host_url = self .upload_info['endPoint' ].split("//" ) host = self .upload_info['bucket' ] + "." + host_url[1 ] callback_var = {"x:var1" : "false" } callback_var_b64 = b64_encode(dumps(callback_var)) callback_host = re.match (r'https?://(\w+\.\w+\.(com|cn))/.*' , self .upload_info['callbackUrl' ])[1 ] 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)) 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" }
用到的函数:
1 2 3 4 5 6 def dumps (dic ): return json.dumps(dic, separators=(',' , ':' )) def b64_encode (text ): return base64.b64encode(text.encode('utf-8' )).decode('utf-8' )
Authorization 就是拼接一下签名:
1 2 3 authorization = f"OSS {self.upload_info['accessKeyId' ]} :{signature} " upload_headers['Authorization' ] = authorization
四、发送请求,获取结果 上传我们的图片到该社区的阿里云 OSS:
1 2 3 with open ("D:\UserData\Desktop\1.jpg" , "rb" ) as f: upload_result = requests.put(url, data=f.read(), headers=upload_headers)
最后大功告成!美滋滋