天天看點

OSS 實踐篇-OSS API 鑒權剖析

背景

使用過阿裡雲 OSS 存儲 API 的使用者都知道,如果 OSS 是私有的權限,需要進行驗簽才能通路。驗簽過程要求用戶端請求的 http request header 中有一個 Authorization(鑒權) 的 header,計算的複雜性和帶來的很多問題讓客戶頭痛不已,尤其 OSS Authorization 頭的計算,今天帶這大家剖析下鑒權的 API。

使用規範預熱

官網鑒權文章:

header 簽名

[URL 簽名](URL 中攜帶簽名。

https://help.aliyun.com/document_detail/31952.html) PutObject 規範

簽名差別

Header URL
不支援設定 expires ,但是要求請求時間不能超過 15min 支援設定 expires
常用 method GET、POST、PUT 常用 method GET、PUT
Date 時間是 GMT 格式 Date 替換成 expires 變成時間戳
signature 不需要 URL encode signature 需要 URL encode

鑒權名詞

AccessKey

簡稱 AK ,通路雲産品的憑證,類似寶藏的鎖,可以是主賬号或者 RAM 子賬号的。

Access Key Secret

簡稱 SK,通路雲産品的秘鑰,類似寶藏的鑰匙,可以是主賬号或者 RAM 子賬号的。

Authorization

Header 來包含簽名(Signature)資訊,表明這個消息已被授權。

Signature

經過各種計算得到的鑒權指紋資訊。

CanonicalizedOSSHeaders

通路 OSS 時,使用者想要加的自定義頭,必須以 "x-oss-" 為頭的字首。如果使用,也必須要加到 Signature 中計算。

CanonicalizedResource

通路 OSS 的資源 object,結構是 /bucket/ object,如下:

  • /zhangyibo/Japan/video/tokhot.avi 将視訊上傳到 bucket 為 zhangyibo 的虛拟目錄 Japan 下面,命名為 tokhot.avi ,可以是 PUT / GET 等操作。
  • /zhangybi/ 針對 bucket 是 zhangyibo 進行的操作,可以是 PUT / GET 等操作。

主賬号 AK SK 擷取方式

OSS 實踐篇-OSS API 鑒權剖析

子賬号 AK SK擷取方式

進入到 RAM 通路控制台,找到對應的子賬号

OSS 實踐篇-OSS API 鑒權剖析

計算鑒權

當客戶通過 header 或者 URL 中自簽名計算 signature 時,經常會遇到計算簽名失敗 “The request signature we calculated does not match the signature you provided” ,可以參考以下 demo 示範了如何調用 API 自簽名時上傳 Object 到 OSS,注意簽名和 header 加入的内容。

使用方法

$PSA1#: python Signature.py -h
Usage: beiwo.py [options]

Options:
  -h, --help  show this help message and exit
  -i AK       Must fill in Accesskey          通路雲産品的 Accesskey
  -k SK       Must fill in AccessKeySecrety   通路雲産品的 Accesskey Secret
  -e ED       Must fill in endpoint           OSS 的 endpoint 地理資訊
  -b BK       Must fill in bucket             OSS bucket 
  -o OBJECTS  File name uploaded to oss       上傳的 object 名稱
  -f FI       Must fill localfile path        本地檔案的名稱           
#! /us/bin/env python
#Author: hanli
#Update: 2018-09-29

from optparse import OptionParser
import urllib, urllib2
import datetime
import base64
import hmac
import sha
import os
import sys
import time


class Main():

# Initial input parse

def __init__(self,options):

  self.ak = options.ak
  self.sk = options.sk
  self.ed = options.ed
  self.bk = options.bk
  self.fi = options.fi
  self.oj = options.objects
  self.left = '\033[1;31;40m'
  self.right = '\033[0m'
  self.types = "application/x-www-form-urlencoded"    
  self.url = 'http://{0}.{1}/{2}'.format(self.bk,self.ed,self.oj)

# Check client input parse

def CheckParse(self):

  if (self.ak and self.sk and self.ed and self.bk and self.oj and self.fi) != None:
    if str(self.ak and self.sk and self.ed and self.bk and self.oj and self.fi):
      self.PutObject()
  else:
    self.ConsoleLog("error","Input parameters cannot be empty")

# GET local GMT time

def GetGMT(self):

  SRM = datetime.datetime.utcnow()
  GMT = SRM.strftime('%a, %d %b %Y %H:%M:%S GMT')

  return GMT

# GET Signature

def GetSignature(self):

  mac = hmac.new("{0}".format(self.sk),"PUT\n\n{0}\n{1}\n/{2}/{3}".format(self.types,self.GetGMT(),self.bk,self.oj), sha)
  Signature = base64.b64encode(mac.digest())

  return Signature

# PutObject

def PutObject(self):

  try: 
    with open(self.fi) as fd:
      files = fd.read()
  except Exception as e:
    self.ConsoleLog("error",e)

  try:
    request = urllib2.Request(self.url, files)
    request.add_header('Host','{0}.{1}'.format(self.bk,self.ed))
    request.add_header('Date','{0}'.format(self.GetGMT()))
    request.add_header('Authorization','OSS {0}:{1}'.format(self.ak,self.GetSignature()))
    request.get_method = lambda:'PUT'
    response = urllib2.urlopen(request,timeout=10)
    fd.close()
    self.ConsoleLog(response.code,response.headers)
  except Exception,e:
    self.ConsoleLog("error",e)

# output error log

def ConsoleLog(self,level=None,mess=None):

  if level == "error":
    sys.exit('{0}[ERROR:]{1}{2}'.format(self.left,self.right,mess))
  else:
    sys.exit('\nHTTP/1.1 {0} OK\n{1}'.format(level,mess))

if __name__ == "__main__":

parser = OptionParser()
parser.add_option("-i",dest="ak",help="Must fill in Accesskey")
parser.add_option("-k",dest="sk",help="Must fill in AccessKeySecrety")
parser.add_option("-e",dest="ed",help="Must fill in endpoint")
parser.add_option("-b",dest="bk",help="Must fill in bucket")
parser.add_option("-o",dest="objects",help="File name uploaded to oss")
parser.add_option("-f",dest="fi",help="Must fill localfile path")

(options, args) = parser.parse_args()
handler = Main(options)
handler.CheckParse()           

### 請求頭

PUT /yuntest HTTP/1.1
Accept-Encoding: identity
Content-Length: 147
Connection: close
User-Agent: Python-urllib/2.7
Date: Sat, 22 Sep 2018 04:36:52 GMT
Host: yourBucket.oss-cn-shanghai.aliyuncs.com
Content-Type: application/x-www-form-urlencoded
Authorization: OSS B0g3mdt:lNCA4L0P43Ax           

響應頭

HTTP/1.1 200 OK
Server: AliyunOSS
Date: Sat, 22 Sep 2018 04:36:52 GMT
Content-Length: 0
Connection: close
x-oss-request-id: 5BA5C6E4059A3C2F
ETag: "D0CAA153941AAA1CBDA38AF"
x-oss-hash-crc64ecma: 8478734191999037841
Content-MD5: 0MqhU5QbIp3Ujqqhy9o4rw==
x-oss-server-time: 15           

注意事項

1、Signature 中所有加入計算的參數都要放在 header 中,保持 header 和 Signature 一緻。

2、PUT 上傳時,Signature 計算的 Content-Type 必須是 application/x-www-form-urlencoded 。

3、通過header 方式進行簽名認證時無法設定過期時間。目前隻有 SDK 、URL 簽名支援設定過期時間。

4、使用者想要保證檔案一緻性,可以在請求頭增加 Content-MD5,但是不注意的人就會忘記加了,補充如下:根據協定 RFC 1864 對消息内容(不包括頭部)計算 MD5 值獲得 128 比特位數字,對該數字進行 base64 編碼為一個消息的 Content-MD5 值,并且 MD5 是 大寫。

5、如果使用者想要單獨加項目 CanonicalizedOSSHeaders 一定要記得不僅在 Header 中加,你的 hmac 計算時也要加

hmac.new("5Lic5Lqs5LiA54K56YO95LiN54Ot","PUT\n\napplication/x-www-form-urlencoded\nSun, 02 Sep 2018 03:20:05 GMT\nx-oss-video:tokhot.avi/zhangyibo/tokhot.avi", sha)           

6、如果遇到 client 計算的 MD5 和 Server 不一緻的情況請直接使用 HTTPS 傳輸,很可能中間的網絡設定有故障或者劫持時導緻記憶體被篡改,隻要将 url 改為

https://

就是啟動 HTTPS 協定 上傳/ 下載下傳 了。

常見案例

通過微信小程式請求 OSS 傳回簽名失敗,通過浏覽器正常

1、隻要通過浏覽器通路,鑒權通過就證明 OSS 的簽名校驗是正常的沒有問題,可以先排除掉 OSS 端。

2、用戶端一定要在微信小程式上部署 HTTP 抓包,對後續分析很重要,抓包中可以看到所有的請求頭和請求參數。

3、通過浏覽器通路時的 HTTP 抓包。

OSS 實踐篇-OSS API 鑒權剖析

結論:

1、通過 403 和 200 的抓包反複對比發現,通過小程式發出的 HTTP 請求和浏覽器發起的 HTTP 請求的 URL 、signature、expires 都一樣,唯一的差別就是微信小程式攜帶了 Content-type ,而通過 Chrom 的請求是沒有攜帶 Content-type,懷疑矛頭指向了這裡。

2、經過代碼确認,發現 signature 計算時是沒有包含 Content-tpye 頭的,而小程式發起的請求攜帶的 Content-tpye ,OSS 收到後會按照攜帶了 Content-tpye 去計算 signature ,是以每次計算都不一樣。

結尾:

遇到類似問題,抓包是最能快速看到問題的。同時也必須要了解下 OSS 請求 header 中攜帶了 Content-tpye ,那麼 signature 計算就要加上 Content-tpye ,保持一緻。

多個 OSS SDK 測試,在 CDN 結合 OSS 場景時,用戶端使用 CDN 域名計算 signature,發起 HEAD 請求,OSS 收到後傳回 403

OSS 實踐篇-OSS API 鑒權剖析

出現這個問題不區分什麼 SDK 都會出現,問題原因是由于用戶端發起的 HEAD 請求在通過 CDN 回原到 OSS 時,CDN 回原是用的 GET 請求,而 OSS 收到時就用 GET 請求方式去計算簽名,得到的結果肯定和用戶端計算不一緻,可以更新到阿裡雲 CDN 處理。以上分析隻适合上述場景。

問題可以通過 tcpdump 抓包或者 Wireshark 對比一下即可知道。

繼續閱讀