您只需在发送给OSS的请求中携带相应的Callback参数即可实现回调。本文介绍Callback的实现原理。
注意事项
华东1(杭州)、华东2(上海)、华北1(青岛)、华北2(北京)、华北 3(张家口)、华北5(呼和浩特)、华北6(乌兰察布)、华南1(深圳)、华南2(河源)、华南3(广州)、西南1(成都)、中国香港、美国(硅谷)、美国(弗吉尼亚)、日本(东京)、新加坡、澳大利亚(悉尼)关停中、马来西亚(吉隆坡)、印度尼西亚(雅加达)、菲律宾(马尼拉)、德国(法兰克福)、英国(伦敦)、阿联酋(迪拜)地域支持设置Callback。
仅PutObject、PostObject和CompleteMultipartUpload接口支持设置Callback。
Callback请求默认超时时间为5秒。
Callback请求失败,不支持自动重试。
步骤1:构造回调参数
Callback参数
Callback参数是由一段经过Base64编码的JSON字符串(字段)构成的。构建callback参数的关键是指定请求回调的服务器URL(callbackUrl)以及回调的内容(callbackBody)。
JSON字段的详细信息请参见下表。
字段
是否必选
描述
callbackUrl
是
文件上传成功后,OSS向此URL发送回调请求。
请求方法为POST,Body为callbackBody指定的内容。正常情况下,该URL需要响应
HTTP/1.1 200 OK
,Body必须为JSON格式,响应头Content-Length必须为合法的值,且大小不超过3 MB。支持同时配置最多5个URL,多个URL间以分号(;)分隔。OSS会依次发送请求,直到第一个回调请求成功返回。
支持HTTPS地址。
为了保证正确处理中文等情况,callbackUrl需做URL编码处理,例如
http://example.com/中文.php?key=value&中文名称=中文值
需要编码为http://example.com/%E4%B8%AD%E6%96%87.php?key=value&%E4%B8%AD%E6%96%87%E5%90%8D%E7%A7%B0=%E4%B8%AD%E6%96%87%E5%80%BC
。
callbackHost
否
发起回调请求时Host头的值,格式为域名或IP地址。
callbackHost仅在设置了callbackUrl时有效。
如果没有配置callbackHost,则解析callbackUrl中的URL,并将解析的Host填充到callbackHost中。
callbackBody
是
发起回调时请求Body的值,例如
key=${object}&etag=${etag}&my_var=${x:my_var}
。callbackBody支持OSS系统参数、自定义变量和常量。
在PutObject和CompleteMultipart中,自定义变量通过callback-var传递。
在PostObject中,各个变量通过表单域传递。
callbackSNI
否
客户端发起回调请求时,OSS是否向通过callbackUrl指定的回源地址发送服务器名称指示SNI(Server Name Indication)。是否发送SNI取决于服务器的配置和需求。对于使用同一个IP地址来托管多个TLS/SSL证书的服务器的情况,建议选择发送SNI。callbackSNI取值如下:
true:发送SNI。
false(默认值):不发送SNI。
说明OSS通过英国(伦敦)地域发起回调请求时,无论callbackSNI取值为true或者false,均会发送SNI。除英国(伦敦)地域以外,OSS支持的其他地域遵守callbackSNI取值约定的行为。
callbackBodyType
否
发起回调请求的Content-Type。Content-Type支持以下两种类型:
application/x-www-form-urlencoded(默认值)
将经过URL编码的值替换callbackBody中的变量。
application/json
按照JSON格式替换callbackBody中的变量。
JSON字段示例如下:
示例一(包含必选参数和可选参数)
{ "callbackUrl":"172.16.XX.XX/test.php", "callbackHost":"oss-cn-hangzhou.aliyuncs.com", "callbackBody":"{\"mimeType\":${mimeType},\"size\":${size}}", "callbackBodyType":"application/json", "callbackSNI":false }
示例二(仅包含必选参数)
{ "callbackUrl":"172.16.XX.XX:23456/index.html", "callbackBody":"bucket=${bucket}&object=${object}&etag=${etag}&size=${size}&mimeType=${mimeType}&imageInfo.height=${imageInfo.height}&imageInfo.width=${imageInfo.width}&imageInfo.format=${imageInfo.format}&x:my_var=${x:my_var}" }
callbackBody中可以设置的系统参数请参见下表。
系统参数
含义
bucket
存储空间名称。
object
对象(文件)的完整路径。
etag
文件的ETag,即返回给用户的ETag字段。
size
Object大小。调用CompleteMultipartUpload时,size为整个Object的大小。
mimeType
资源类型,例如jpeg图片的资源类型为image/jpeg。
imageInfo.height
图片高度。该变量仅适用于图片格式,对于非图片格式,该变量的值为空。
imageInfo.width
图片宽度。该变量仅适用于图片格式,对于非图片格式,该变量的值为空。
imageInfo.format
图片格式,例如JPG、PNG等。该变量仅适用于图片格式,对于非图片格式,该变量的值为空。
crc64
与上传文件后返回的x-oss-hash-crc64ecma头内容一致。
contentMd5
与上传文件后返回的Content-MD5头内容一致。
重要仅在调用PutObject和PostObject接口上传文件时,该变量的值不为空。
vpcId
发起请求的客户端所在的VpcId。如果不是通过VPC发起请求,则该变量的值为空。
clientIp
发起请求的客户端IP地址。
reqId
发起请求的RequestId。
operation
发起请求的接口名称,例如PutObject、PostObject等。
callback-var自定义参数
您可以通过callback-var参数来配置自定义参数。自定义参数是一个Key-Value对(例如:
my_var=${x:my_var}
)。在OSS发起POST回调请求的时候,会将callback-var自定义参数以及上述系统参数放在POST请求的body中,方便用户接收回调信息。构造自定义参数的方法和callBack参数的方法相同,也是以JSON格式来传递。该JSON字符串是一个包含所有自定义参数的Key-Value对。
重要自定义参数的Key必须以
x:
开头且为小写,否则即使OSS返回200,自定义参数也不能被正确赋值。
步骤2:构造回调请求
将Callback和callback-var参数附加到OSS请求时,需要对上述构造的JSON字符串使用base64编码,然后选用以下任意一种方式将参数附加到OSS的请求中。
构造callback(x-oss-callback)
构造回调参数。
{ "callbackUrl": "http://xxx.xxx.22.143/test", "callbackHost": "your.callback.com", "callbackBody": "bucket=${bucket}&object=${object}&my_var=${x:my_var}", "callbackBodyType": "application/x-www-form-urlencoded", "callbackSNI": false }
将上面的JSON字符串使用Base64进行编码后得到编码后的结果callback(x-oss-callback)。
ewogICAg****bGxiYWNrVXJsIjogImh0dHA6Ly94eHgueHh4LjIyLjE0My90ZXN0IiwKICAgICJjYWxsYmFja0hvc3QiOiAieW91ci5jYWxsYmFjay5jb20iLAogICAgImNhbGxiYWNrQm9keSI6ICJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyPSR7eDpteV92YXJ9IiwKICAgICJjYWxsYmFja0JvZHlUeXBlIjogImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIsCiAgICAiY2FsbGJhY2tTTkkiOiBmYWxzZQp9
构造callback-var(x-oss-callback-var)
构造回调参数。
{"x:my_var": "var"}
将自定义变量使用Base64进行编码后得到编码后的结果callback-var(x-oss-callback-var)。
eyJ4Om15X3ZhciI6ICJ2YX****==
在URL中携带参数
在URL中携带参数时,callback
为必选参数,callback-var
为可选参数。如果请求中出现了callback
或callback-var
,则在计算签名时,需要将callback或者callback-var参数作为CanonicalizedResource的子资源。更多信息,请参见构建CanonicalizedResource的方法。
PUT /your_object?OSSAccessKeyId=LTAI5t7h6SgiLSga****&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv****&Expires=1682484377&callback-var=eyJ4Om15X3ZhciI6ICJ2YX****==&callback=bGxiYWNrVXJsIjogImh0dHA6Ly94eHgueHh4LjIyLjE0My90ZXN0IiwKICAgICJjYWxsYmFja0hvc3QiOiAieW91ci5jYWxsYmFjay5jb20iLAogICAgImNhbGxiYWNrQm9keSI6ICJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyPSR7eDpteV92YXJ9IiwKICAgICJjYWxsYmFja0JvZHlUeXBlIjogImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIsCiAgICAiY2FsbGJhY2tTTkkiOiBmYWxzZQp9 HTTP/1.1
Host: callback-test.oss-cn-hangzhou.aliyuncs.com
Date: Wed, 26 Apr 2023 03:46:17 GMT
Content-Length: 5
Content-Type: text/plain
在Header中携带参数
在Header中携带参数时,需要将x-oss-callback
或者x-oss-callback-var
作为Header带入请求发送。在计算签名CanonicalizedOSSHeaders时,将x-oss-callback-var和x-oss-callback计算在内。
完整示例如下:
PUT /your_object HTTP/1.1
Host: callback-test.oss-test.aliyun-inc.com
Accept-Encoding: identity
Content-Length: 5
x-oss-callback-var: eyJ4Om15X3ZhciI6ICJ2YX****==
User-Agent: aliyun-sdk-python/0.4.0 (Linux/2.6.32-220.23.2.ali1089.el5.x86_64/x86_64;2.5.4)
x-oss-callback: ewogICAg****bGxiYWNrVXJsIjogImh0dHA6Ly94eHgueHh4LjIyLjE0My90ZXN0IiwKICAgICJjYWxsYmFja0hvc3QiOiAieW91ci5jYWxsYmFjay5jb20iLAogICAgImNhbGxiYWNrQm9keSI6ICJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyPSR7eDpteV92YXJ9IiwKICAgICJjYWxsYmFja0JvZHlUeXBlIjogImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIsCiAgICAiY2FsbGJhY2tTTkkiOiBmYWxzZQp9
Host: callback-test.oss-test.aliyun-inc.com
Expect: 100-Continue
Date: Wed, 26 Apr 2023 03:46:17 GMT
Content-Type: text/plain
Authorization: OSS qn6q**************:77Dv****************
Test
在POST请求的Body中使用表单域来携带参数
使用PostObject接口上传Object时,仅支持通过POST请求的Body中使用表单域来携带参数的方式指定回调参数。
如果需要在POST上传Object时附带回调参数,callback参数要使用独立的表单域来附加,示例如下:
--9431149156168 Content-Disposition: form-data; name="callback" eyJjYWxsYmFja1VybCI6IjEwLj****4xN
如果是自定义参数,不能直接将callback-var参数直接附加到表单域中。每个自定义的参数都需要使用独立的表单域来添加。例如,自定义参数JSON字段示例如下:
{ "x:var1":"value1", "x:var2":"value2" }
则POST请求的表单域为:
--9431149156168 Content-Disposition: form-data; name="callback" eyJjYWxsYmFja1VybCI6IjEwLj****4xN --9431149156168 Content-Disposition: form-data; name="x:var1" value1 --9431149156168 Content-Disposition: form-data; name="x:var2" value2
同时,您还可以在policy中添加callback条件。如果不添加callback条件,则不对该参数做上传验证,如下所示。
{ "expiration": "2021-12-01T12:00:00.000Z", "conditions": [ {"bucket": "johnsmith" }, {"callback": "eyJjYWxsYmFja1V****4My"}, ["starts-with", "$key", "user/eric/"], ] }
步骤3:发起回调请求
如果文件上传成功,OSS会根据请求中的callback参数和callback-var自定义参数,将特定内容以POST方式发送给应用服务器。
POST /test HTTP/1.1
Host: your.callback.com
Connection: close
Authorization: GevnM3**********3j7AKluzWnubHSVWI4dY3VsIfUHYWnyw==
Content-MD5: iKU/O/JB***ZMd8Ftg==
Content-Type: application/x-www-form-urlencoded
Date: Tue, 07 May 2024 03:06:13 GMT
User-Agent: aliyun-oss-callback
x-oss-bucket: your_bucket
x-oss-pub-key-url: aHR0cHM6Ly9nb3NzcHVi**********vY2FsbGJeV92MS5wZW0=
x-oss-request-id: 66399AA50*****3334673EC2
x-oss-requester: 23313******948342006
x-oss-signature-version: 1.0
x-oss-tag: CALLBACK
bucket=your_bucket&object=your_object&my_var=var
(可选)步骤4:回调签名
设置Callback参数后,OSS将按照您设置的callbackUrl发送POST回调请求给用户的应用服务器。应用服务器收到回调请求后,如果希望验证回调请求是否由OSS发起,可以通过在回调中携带签名来验证OSS身份。
验证签名为可选步骤,您可以根据实际情况决定是否验证签名。
步骤一:生成签名
生成签名的方式
采用RSA非对称方式生成签名。
authorization = base64_encode(rsa_sign(private_key, url_decode(path) + query_string + '\n' + body, md5))
说明其中,private_key为私钥,path为回调请求的资源路径,query_string为查询字符串,body为回调的消息体。
生成签名的步骤
获取待签名字符串:资源路径经过URL解码后,会附加原始的查询字符串、一个回车符以及回调消息体。
RSA签名:使用密钥对待签名字符串进行签名,签名的哈希函数为md5。
将签名后的结果做Base64编码,获取最终的签名,然后将签名放在回调请求的authorization头中。
生成签名的示例
POST /index.php?id=1&index=2 HTTP/1.0 Host: 172.16.XX.XX Connection: close Content-Length: 18 authorization: kKQeGTRccDKyHB3H9vF+xYMSrmhMZj****/kdD1ktNVgbWEfYTQG0G2SU/RaHBovRCE8OkQDjC3uG33esH2t**** Content-Type: application/x-www-form-urlencoded User-Agent: http-client/0.0.1 x-oss-pub-key-url: aHR0cDovL2dvc3NwdWJsaWMuYWxpY2RuLmNvbS9jYWxsYmFja19wdWJfa2V5X3YxLnsr**** bucket=examplebucket
path为
/index.php
,query_string为?id=1&index=2
,body为bucket=examplebucket
,最终签名结果为kKQeGTRccDKyHB3H9vF+xYMSrmhMZjzzl2/kdD1ktNVgbWEfYTQG0G2SU/RaHBovRCE8OkQDjC3uG33esH2t****
。
步骤二:验证签名
验证签名的方式
通过应用服务器验证签名示例如下:
Result = rsa_verify(public_key, md5(url_decode(path) + query_string + ‘\n’ + body), base64_decode(authorization))
其中,public_key为公钥,authorization为回调头中的签名。
验证签名的过程
回调请求的
x-oss-pub-key-url
头保存的是公钥URL地址的base64编码,需要对x-oss-pub-key-url
执行base64解码后获取公钥。public_key = urlopen(base64_decode(x-oss-pub-key-url头的值))
例如,获取到公钥的URL地址为
aHR0cDovL2dvc3NwdWJsaWMuYWxpY2RuLmNvbS9jYWxsYmFja19wdWJfa2V5X3YxLnBlbQ==
,经过base64解码后得到http://gosspublic.alicdn.com/callback_pub_key_v1.pem
。说明OSS颁发的public_key中
x-oss-pub-key-url
头的值必须以http://gosspublic.alicdn.com/
或者https://gosspublic.alicdn.com/
开头。由于公钥地址的内容不变,因此推荐根据公钥地址缓存公钥内容,以避免因为网络或者公钥地址所在服务问题造成的业务异常。获取base64解码后的签名。
signature = base64_decode(authorization头的值)
获取待签名字符串,方法与签名一致。
sign_str = url_decode(path) + query_string + ‘\n’ + body
验证签名。
result = rsa_verify(public_key, md5(sign_str), signature)
验证签名示例
以Python为例,演示应用服务器中验证签名的方法,此示例需要安装M2Crypto库。
import httplib import base64 import md5 import urllib2 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from M2Crypto import RSA from M2Crypto import BIO def get_local_ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "" class MyHTTPRequestHandler(BaseHTTPRequestHandler): ''' def log_message(self, format, *args): return ''' def do_POST(self): #get public key pub_key_url = '' try: pub_key_url_base64 = self.headers['x-oss-pub-key-url'] pub_key_url = pub_key_url_base64.decode('base64') if not pub_key_url.startswith("http://gosspublic.alicdn.com/") and not pub_key_url.startswith("https://gosspublic.alicdn.com/"): self.send_response(400) self.end_headers() return url_reader = urllib2.urlopen(pub_key_url) #you can cache it,recommend caching public key content based on the public key address pub_key = url_reader.read() except: print 'pub_key_url : ' + pub_key_url print 'Get pub key failed!' self.send_response(400) self.end_headers() return #get authorization authorization_base64 = self.headers['authorization'] authorization = authorization_base64.decode('base64') #get callback body content_length = self.headers['content-length'] callback_body = self.rfile.read(int(content_length)) #compose authorization string auth_str = '' pos = self.path.find('?') if -1 == pos: auth_str = urllib2.unquote(self.path) + '\n' + callback_body else: auth_str = urllib2.unquote(self.path[0:pos]) + self.path[pos:] + '\n' + callback_body print auth_str #verify authorization auth_md5 = md5.new(auth_str).digest() bio = BIO.MemoryBuffer(pub_key) rsa_pub = RSA.load_pub_key_bio(bio) try: result = rsa_pub.verify(auth_md5, authorization, 'md5') except: result = False if not result: print 'Authorization verify failed!' print 'Public key : %s' % (pub_key) print 'Auth string : %s' % (auth_str) self.send_response(400) self.end_headers() return #do something according to callback_body #response to OSS resp_body = '{"Status":"OK"}' self.send_response(200) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(resp_body))) self.end_headers() self.wfile.write(resp_body) class MyHTTPServer(HTTPServer): def __init__(self, host, port): HTTPServer.__init__(self, (host, port), MyHTTPRequestHandler) if '__main__' == __name__: server_ip = get_local_ip() server_port = 23451 server = MyHTTPServer(server_ip, server_port) server.serve_forever()
其他语言的服务端代码请参见下表。
SDK语言
描述
Java
下载地址:Java
运行方法:解压包运行
java -jar oss-callback-server-demo.jar 9000
(9000指运行的端口,可以自行指定)。
Python
下载地址:Python
运行方法:解压包直接运行
python callback_app_server.py
,运行该程序需要安装RSA的依赖。
Go
下载地址:Go
运行方法:解压后参看README.md。
PHP
下载地址:PHP
运行方法:部署到Apache环境下,因为PHP本身语言的特点,取一些数据头部会依赖于环境。所以可以参考例子根据所在环境修改。
.NET
下载地址:.NET
运行方法:解压后参看
README.md
。
Node.js
下载地址:Node.js
运行方法:解压包直接运行
node example.js
。
Ruby
下载地址:Ruby
运行方法:ruby aliyun_oss_callback_server.rb
步骤5:返回回调结果
应用服务器返回响应给OSS。返回的回调请求为:
HTTP/1.0 200 OK
Server: BaseHTTP/0.3 Python/2.7.6
Date: Mon, 14 Sep 2015 12:37:27 GMT
Content-Type: application/json
Content-Length: 9
{"a":"b"}
应用服务器返回OSS的响应必须带有Content-Length的Header,Body大小不能超过1 MB。
步骤6:返回上传结果
OSS将应用服务器返回的内容返回给用户。
返回的内容响应为:
HTTP/1.1 200 OK
Date: Mon, 14 Sep 2015 12:37:27 GMT
Content-Type: application/json
Content-Length: 9
Connection: keep-alive
ETag: "D8E8FCA2DC0F896FD7CB4CB0031BA249"
Server: AliyunOSS
x-oss-bucket-version: 1442231779
x-oss-request-id: 55F6BF87207FB30F2640C548
{"a":"b"}
对于CompleteMultipartUpload
请求,在返回请求body中存在内容(例如XML格式的信息),使用上传回调功能后会覆盖原有的body中的内容,例如{"a":"b"}
。
错误码
错误码 | HTTP状态码 | 描述 |
InvalidArgument | 400 | 传入的Callback或者Callback-var参数不合法。不合法的情况的分类如下:
|
CallbackFailed | 203 | 文件已经成功上传到了OSS,但回调失败。回调失败表示OSS没有收到预期的回调响应(例如,应用服务器返回的内容不是JSON格式),不能说明应用服务器没有收到回调请求 |
常见问题
OSS上传文件失败后是否会发送回调通知给应用服务器?
不会。OSS上传文件成功后会执行回调,如果上传失败,则不执行回调,直接返回错误信息。
报错Response body is not valid json format.如何处理?
应用服务器处理过程中抛出异常,导致返回给OSS的Body不是JSON格式,如下图所示:
解决方法:
通过以下命令确认内容。
curl -d "<Content>" <CallbackServerURL> -v
通过抓包确认内容。
Windows下推荐使用工具Wireshark抓包,Linux下使用命令tcpdump抓包。
应用服务器返回给OSS的Body中带有BOM头。
这类错误常见于使用PHP SDK编写的应用服务器中。由于PHP SDK返回了BOM头,导致OSS收到的Body中多了三个字节,出现不符合JSON格式的Body。如下图所示,ef bb bf这三个字节为BOM头。
解决方法:删除应用服务器返回OSS Body中的BOM头。