Callback

您只需在发送给OSS的请求中携带相应的Callback参数即可实现回调。本文介绍Callback的实现原理。

注意事项

  • 华东1(杭州)、华东2(上海)、华北1(青岛)、华北2(北京)、华北 3(张家口)、华北5(呼和浩特)、华北6(乌兰察布)、华南1(深圳)、华南2(河源)、华南3(广州)、西南1(成都)、中国香港、美国(硅谷)、美国(弗吉尼亚)、日本(东京)、新加坡、马来西亚(吉隆坡)、印度尼西亚(雅加达)、菲律宾(马尼拉)、德国(法兰克福)、英国(伦敦)、阿联酋(迪拜)地域支持设置Callback。

  • PutObjectPostObjectCompleteMultipartUpload接口支持设置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)

    1. 构造回调参数。

      {
          "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
      }
    2. 将上面的JSON字符串使用Base64进行编码后得到编码后的结果callback(x-oss-callback)。

      ewogICAg****bGxiYWNrVXJsIjogImh0dHA6Ly94eHgueHh4LjIyLjE0My90ZXN0IiwKICAgICJjYWxsYmFja0hvc3QiOiAieW91ci5jYWxsYmFjay5jb20iLAogICAgImNhbGxiYWNrQm9keSI6ICJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyPSR7eDpteV92YXJ9IiwKICAgICJjYWxsYmFja0JvZHlUeXBlIjogImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIsCiAgICAiY2FsbGJhY2tTTkkiOiBmYWxzZQp9
  • 构造callback-var(x-oss-callback-var)

    1. 构造回调参数。

      {"x:my_var": "var"}
    2. 将自定义变量使用Base64进行编码后得到编码后的结果callback-var(x-oss-callback-var)。

      eyJ4Om15X3ZhciI6ICJ2YX****==

在URL中携带参数

在URL中携带参数时,callback为必选参数,callback-var为可选参数。如果请求中出现了callbackcallback-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为回调的消息体。

  • 生成签名的步骤

    1. 获取待签名字符串:资源路径经过URL解码后,会附加原始的查询字符串、一个回车符以及回调消息体。

    2. RSA签名:使用密钥对待签名字符串进行签名,签名的哈希函数为md5。

    3. 将签名后的结果做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为回调头中的签名。

  • 验证签名的过程

    1. 回调请求的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/开头。由于公钥地址的内容不变,因此推荐根据公钥地址缓存公钥内容,以避免因为网络或者公钥地址所在服务问题造成的业务异常。

    2. 获取base64解码后的签名。

      signature = base64_decode(authorization头的值)
    3. 获取待签名字符串,方法与签名一致。

      sign_str = url_decode(path) + query_string + ‘\n’ + body
    4. 验证签名。

      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中存在内容(例如JSON格式的信息),使用上传回调功能后会覆盖原有的body中的内容,例如{"a":"b"}

错误码

错误码

HTTP状态码

描述

InvalidArgument

400

传入的Callback或者Callback-var参数不合法。不合法的情况的分类如下:

  • 在PutObject和CompleteMultipartUpload接口中URL和Header同时传入Callback(x-oss-callback)或者Callback-var(x-oss-callback-var)。

  • Callback或者Callback-var参数过长,超过5 KB。

  • Callback或者Callback-var参数没有经过Base64编码或经过Base64解码后不是合法的JSON格式。

  • Callback参数解析后,callbackUrl字段包含的URL超过限制(5个),或者URL中传入的端口不合法,示例如下:

    {"callbackUrl":"172.16.XX.XX:test",
        "callbackBody":"test"}
  • Callback参数解析后callbackBody字段为空。

  • Callback参数解析后callbackBodyType字段的值不是application/x-www-form-urlencoded或者application/json

  • Callback参数解析后callbackBody字段中变量的格式不合法,合法的格式为${var}。

  • Callback-var参数解析后不是预期的JSON格式,预期的格式为{"x:var1":"value1","x:var2":"value2"}

CallbackFailed

203

文件已经成功上传到了OSS,但回调失败。回调失败表示OSS没有收到预期的回调响应(例如,应用服务器返回的ContentType为application/json,但是内容不是JSON格式),不能说明应用服务器没有收到回调请求

常见问题

OSS上传文件失败后是否会发送回调通知给应用服务器?

不会。OSS上传文件成功后会执行回调,如果上传失败,则不执行回调,直接返回错误信息。

报错Response body is not valid json format.如何处理?

  • 应用服务器处理过程中抛出异常,导致返回给OSS的Body不是JSON格式,如下图所示:callback

    解决方法:

    • 通过以下命令确认内容。

      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头。

    callback1

    解决方法:删除应用服务器返回OSS Body中的BOM头。