用戶只需要在發送給OSS的請求中攜帶相應的Callback參數,即能實現回調。

現在支援CallBack的API 介面有:PutObject、PostObject、CompleteMultipartUpload。

構造CallBack參數

CallBack參數是由一段經過base64編碼的Json字串,用戶關鍵需要指定請求回調的伺服器URL(callbackUrl)以及回調的內容(callbackBody)。詳細的Json欄位如下:

欄位 含義 選項
callbackUrl
  • 檔案上傳成功後OSS向此url發送回調請求,要求方法為POST,body為callbackBody指定的內容。正常情況下,該url需要響應“HTTP/1.1 200 OK”,body必須為JSON格式,回應標頭Content-Length必須為合法的值,且不超過3MB。
  • 支援同時配置最多5個url,以”;”分割。OSS會依次發送請求直到第一個返回成功為止。
  • 如果沒有配置或者值為空則認為沒有配置callback。
  • 支援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頭的值,只有在設定了callbackUrl時才有效。
  • 如果沒有配置 callbckHost,則會解析callbackUrl中的url並將解析出的host填充到callbackHost中
可選項
callbackBody
  • 發起回調時請求body的值,例如:key=$(key)&etag=$(etag)&my_var=$(x:my_var)。
  • 支援OSS系統變數、自訂變數和常量,支援的系統變數如下表所示 。自訂變數的支援方式在PutObject和CompleteMultipart中是通過callback-var來傳遞,在PostObject中則是將各個變數通過表單域來傳遞。
必選項
callbackBodyType
  • 發起回調請求的Content-Type,支援application/x-www-form-urlencoded和application/json,預設為前者。
  • 如果為application/x-www-form-urlencoded,則callbackBody中的變數將會被經過url編碼的值替換掉,如果為application/json,則會按照json格式替換其中的變數。
可選項

樣本json串如下

{
"callbackUrl":"121.101.166.30/test.php",
"callbackHost":"oss-cn-hangzhou.aliyuncs.com",
"callbackBody":"{\"mimeType\":${mimeType},\"size\":${size}}",
"callbackBodyType":"application/json"
}
{
"callbackUrl":"121.43.113.8: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}&my_var=${x:my_var}"
}

其中callbackBody中可以設定的系統變數有,其中imageInfo針對於圖片格式,如果為非圖片格式都為空:

系統變數 含義
bucket bucket
object object
etag 檔案的etag,即返回給用戶的etag欄位
size object大小,CompleteMultipartUpload時為整個object的大小
mimeType 資源類型,如jpeg圖片的資源類型為image/jpeg
imageInfo.height 圖片高度
imageInfo.width 圖片寬度
imageInfo.format 圖片格式,如jpg、png等

自訂參數

用戶可以通過callback-var參數來配置自訂參數。

自訂參數是一個Key-Value的Map,用戶可以配置自己需要的參數到這個Map。在OSS發起POST回調請求的時候,會將這些參數和上一節所述的系統參數一起放在POST請求的body中以方便接收回調方獲取。

構造自訂參數的方法和callback參數的方法是一樣的,也是以json格式來傳遞。該json字元串就是一個包含所有自訂參數的Key-Value的Map。

说明
用戶自訂參數的Key一定要以x:開頭,且必須為小寫。否則OSS會返回錯誤。

假定用戶需要設定兩個自訂的參數分別為x:var1和x:var2,對應的值分別為value1和value2,那麼構造出來的json格式如下:

{
"x:var1":"value1",
"x:var2":"value2"
}

構造回調請求

構造完成上述的callback和callback-var兩個參數之後,一共有三種方式傳給OSS。其中callback為必填參數,callback-var為選擇性參數,如果沒有自訂參數的話可以不用添加callback-var欄位。這三種方式如下:

  • 在URL中攜帶參數。
  • 在Header中攜帶參數。
  • 在POST請求的body中使用表單域來攜帶參數。
    说明
    在使用POST請求上傳Object的時候只能使用這種方式來指定回調參數。

這三種方式只能同時使用其中一種,否則OSS會返回InvalidArgument錯誤。

要將參數附加到OSS的請求中,首先要將上文構造的json字元串使用base64編碼,然後按照如下的方法附加到OSS的請求中:

  • 如果在URL中攜帶參數。把callback=[CallBack]或者callback-var=[CallBackVar]作為一個url參數帶入請求發送。計算簽名CanonicalizedResource時 ,將callback或者callback-var當做一個sub-resource計算在內
  • 如果在Header中攜帶參數。把x-oss-callback=[CallBack]或者x-oss-callback-var=[CallBackVar]作為一個head帶入請求發送。在計算簽名CanonicalizedOSSHeaders時,將x-oss-callback-var和x-oss-callback計算在內。如下樣本:
    PUT /test.txt HTTP/1.1
    Host: callback-test.oss-test.aliyun-inc.com
    Accept-ncoding: identity
    Content-Length: 5
    x-oss-callback-var: eyJ4Om15X3ZhciI6ImZvci1jYWxsYmFjay10ZXN0In0=
    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: eyJjYWxsYmFja1VybCI6IjEyMS40My4xMTMuODoyMzQ1Ni9pbmRleC5odG1sIiwgICJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mZXRhZz0ke2V0YWd9JnNpemU9JHtzaXplfSZtaW1lVHlwZT0ke21pbWVUeXBlfSZpbWFnZUluZm8uaGVpZ2h0PSR7aW1hZ2VJbmZvLmhlaWdodH0maW1hZ2VJbmZvLndpZHRoPSR7aW1hZ2VJbmZvLndpZHRofSZpbWFnZUluZm8uZm9ybWF0PSR7aW1hZ2VJbmZvLmZvcm1hdH0mbXlfdmFyPSR7eDpteV92YXJ9In0=
    Host: callback-test.oss-test.aliyun-inc.com
    Expect: 100-Continue
    Date: Mon, 14 Sep 2015 12:37:27 GMT
    Content-Type: text/plain
    Authorization: OSS mlepou3zr4u7b14:5a74vhd4UXpmyuudV14Kaen5cY4=
    Test
  • 如果需要在POST上傳Object的時候附帶回調參數會稍微複雜一點,callback參數要使用獨立的表單域來附加,如下面的樣本:
    --9431149156168
    Content-Disposition: form-data; name="callback"
    eyJjYWxsYmFja1VybCI6IjEwLjEwMS4xNjYuMzA6ODA4My9jYWxsYmFjay5waHAiLCJjYWxsYmFja0hvc3QiOiIxMC4xMDEuMTY2LjMwIiwiY2FsbGJhY2tCb2R5IjoiZmlsZW5hbWU9JChmaWxlbmFtZSkmdGFibGU9JHt4OnRhYmxlfSIsImNhbGxiYWNrQm9keVR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifQ==

    如果擁有自訂參數的話,不能直接將callback-var參數直接附加到表單域中,每個自訂的參數都需要使用獨立的表單域來附加,舉個例子,如果用戶的自訂參數的json為

    {
    "x:var1":"value1",
    "x:var2":"value2"
    }

    那麼POST請求的表單域應該如下:

    --9431149156168
    Content-Disposition: form-data; name="callback"
    eyJjYWxsYmFja1VybCI6IjEwLjEwMS4xNjYuMzA6ODA4My9jYWxsYmFjay5waHAiLCJjYWxsYmFja0hvc3QiOiIxMC4xMDEuMTY2LjMwIiwiY2FsbGJhY2tCb2R5IjoiZmlsZW5hbWU9JChmaWxlbmFtZSkmdGFibGU9JHt4OnRhYmxlfSIsImNhbGxiYWNrQm9keVR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifQ==
    --9431149156168
    Content-Disposition: form-data; name="x:var1"
    value1
    --9431149156168
    Content-Disposition: form-data; name="x:var2"
    value2

    同時可以在policy中添加callback條件(如果不添加callback,則不對該參數做上傳驗證)如:

    { "expiration": "2014-12-01T12:00:00.000Z",
      "conditions": [
        {"bucket": "johnsmith" },
        {"callback": "eyJjYWxsYmFja1VybCI6IjEwLjEwMS4xNjYuMzA6ODA4My9jYWxsYmFjay5waHAiLCJjYWxsYmFja0hvc3QiOiIxMC4xMDEuMTY2LjMwIiwiY2FsbGJhY2tCb2R5IjoiZmlsZW5hbWU9JChmaWxlbmFtZSkiLCJjYWxsYmFja0JvZHlUeXBlIjoiYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkIn0="},
        ["starts-with", "$key", "user/eric/"],
      ]
    }

發起回調請求

如果檔案上傳成功,OSS會根據用戶的請求中的callback參數和自訂參數(callback-var參數),將特定內容以POST方式發送給應用伺服器。

POST /index.html HTTP/1.0
Host: 121.43.113.8
Connection: close
Content-Length: 181
Content-Type: application/x-www-form-urlencoded
User-Agent: ehttp-client/0.0.1
bucket=callback-test&object=test.txt&etag=D8E8FCA2DC0F896FD7CB4CB0031BA249&size=5&mimeType=text%2Fplain&imageInfo.height=&imageInfo.width=&imageInfo.format=&x:var1=for-callback-test

返回回調結果

比如應用伺服器端返回的回應請求為:

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"}

返回上傳結果

再給客戶端的內容為:

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"},希望對此處做好判斷處理。

回調簽名

使用者佈建callback參數後,OSS將按照使用者佈建的callbackUrl發送POST回調請求給用戶的應用伺服器。應用伺服器收到回調請求之後,如果希望驗證回調請求確實是由OSS發起的話,那麼可以通過在回調中帶上籤名來驗證OSS的身份。

  • 生成簽名

    簽名在OSS端發生,採用RSA非對稱方式簽名,私密金鑰加密的過程為:

    authorization = base64_encode(rsa_sign(private_key, url_decode(path) + query_string + ‘\n’ + body, md5))
    说明
    其中private_key為私密金鑰,只有oss知曉,path為回調請求的資源路徑,query_string為查詢字元串,body為回調的消息體,所以簽名過程由以下幾步組成:
    • 獲取待簽名字元串:資源路徑經過url解碼後,加上原始的查詢字元串,加上一個回車符,加上回調消息體
    • RSA簽名:使用秘鑰對待簽名字元串進行簽名,簽名的hash函數為md5
    • 將簽名後的結果做base64編碼,得到最終的簽名,簽名放在回調請求的authorization頭中
    如下例:
    POST /index.php?id=1&index=2 HTTP/1.0
    Host: 121.43.113.8
    Connection: close
    Content-Length: 18
    authorization: kKQeGTRccDKyHB3H9vF+xYMSrmhMZjzzl2/kdD1ktNVgbWEfYTQG0G2SU/RaHBovRCE8OkQDjC3uG33esH2txA==
    Content-Type: application/x-www-form-urlencoded
    User-Agent: ehttp-client/0.0.1
    x-oss-pub-key-url: aHR0cDovL2dvc3NwdWJsaWMuYWxpY2RuLmNvbS9jYWxsYmFja19wdWJfa2V5X3YxLnBlbQ==
    bucket=yonghu-test

    path為/index.php,query_string為?id=1&index=2,body為bucket=yonghu-test,最終簽名結果為kKQeGTRccDKyHB3H9vF+xYMSrmhMZjzzl2/kdD1ktNVgbWEfYTQG0G2SU/RaHBovRCE8OkQDjC3uG33esH2txA==

  • 驗證簽名

    驗證簽名的過程即為簽名的逆過程,由應用伺服器驗證,過程如下:

    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編碼,因此需要對其做base64解碼後獲取到公開金鑰,即
      public_key = urlopen(base64_decode(x-oss-pub-key-url頭的值))

      這裡需要注意,用戶需要校驗x-oss-pub-key-url頭的值必須以http://gosspublic.alicdn.com/或者https://gosspublic.alicdn.com/開頭,目的是為了保證這個publickey是由OSS頒發的。

    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)

    以上例為例:

    1. 獲取到公開金鑰的url地址,即aHR0cDovL2dvc3NwdWJsaWMuYWxpY2RuLmNvbS9jYWxsYmFja19wdWJfa2V5X3YxLnBlbQ==經過base64解碼後得到http://gosspublic.alicdn.com/callback_pub_key_v1.pem
    2. 簽名頭kKQeGTRccDKyHB3H9vF+xYMSrmhMZjzzl2/kdD1ktNVgbWEfYTQG0G2SU/RaHBovRCE8OkQDjC3uG33esH2txA==做base64解碼(由於為非列印字元,無法顯示出解碼後的結果)
    3. 獲取待簽名字元串,即url_decode(“index.php”) + “?id=1&index=2” + “\n” + “bucket=yonghu-test”,並做md5
    4. 驗證簽名
  • 應用伺服器樣本

    以下為一段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
                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 accoding 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()

    其它語言實現的應用伺服器如下:

    Java版本:

    • 下載地址:點擊這裡
    • 運行方法:解壓包運行java -jar oss-callback-server-demo.jar 9000(9000就啟動並執行通信埠,可以自己指定)

    PHP版本:

    • 下載地址:點擊這裡
    • 運行方法:部署到Apache環境下,因為PHP本身語言的特點,取一些數據頭部會依賴於環境。所以可以參考例子根據所在環境修改。

    Python版本:

    • 下載地址:點擊這裡
    • 運行方法:解壓包直接運行python callback_app_server.py,運行該程式需要安裝rsa的依賴。

    C#版本:

    • 下載地址:點擊這裡
    • 運行方法:解壓後參看 README.md

    Go版本:

    • 下載地址:點擊這裡
    • 運行方法:解壓後參看 README.md

    Go版本:

    • 下載地址:點擊這裡
    • 運行方法:解壓後參看 README.md

    Ruby版本:

    • 下載地址:點擊這裡
    • 運行方法: ruby aliyun_oss_callback_server.rb

特別須知

  • 如果傳入的callback或者callback-var不合法,則會返回400錯誤,錯誤碼為”InvalidArgument”,不合法的情況包括以下幾類:
    • PutObject()和CompleteMultipartUpload()介面中url和header同時傳入callback(x-oss-callback)或者callback-var(x-oss-callback-var)參數
    • callback或者callback-var(PostObject()由於沒有callback-var參數,因此沒有此限制,下同)參數過長(超過5KB)
    • callback或者callback-var沒有經過base64編碼
    • callback或者callback-var經過base64解碼後不是合法的json格式
    • callback參數解析後callbackUrl欄位包含的url超過限制(5個),或者url中傳入的port不合法,比如
      {"callbackUrl":"10.101.166.30: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"...}
  • 如果回調失敗,則返回203,錯誤碼為”CallbackFailed”,回調失敗只是表示OSS沒有收到預期的回調響應,不代表應用伺服器沒有收到回調請求(比如應用伺服器返回的內容不是json格式),另外,此時檔案已經成功上傳到了OSS
  • 應用伺服器返回OSS的響應必須帶有Content-Length的Header,Body大小不要超過1MB。

Callback支援的地域

Callback目前支援的地域如下:華北 2(北京)、華東 1(杭州)、華北 1(青島)、華東 2(上海)、上海金融雲、華南 1(深圳)、香港、華北 5(呼和浩特)、華北 3(張家口)、中東東部 1(迪拜)、亞太東北 1(日本)、歐洲中部 1(法蘭克福)、亞太東南 1(新加坡)、美國東部 1(維吉尼亞)、美國西部 1(矽谷)、亞太東南 2 (悉尼)以及亞太東南 3(吉隆坡)。