雲計算

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 獲取方式

image.png

子賬號 AK SK獲取方式

進入到 RAM 訪問控制檯,找到對應的子賬號

image.png

計算鑑權

當客戶通過 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 抓包。

![image.png](https://ucc.alicdn.com/pic/developer-ecology/120a50410a6447a3a15ce5db9bb00995.png)

#### 結論:
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 

![image.png](https://ucc.alicdn.com/pic/developer-ecology/6f3fb72106ba408fb790ffa85e028456.png)

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

Leave a Reply

Your email address will not be published. Required fields are marked *