如何設計一個API簽名

  • 2019 年 12 月 22 日
  • 筆記

前言

大部分情況下,我們使用已有的API簽名方案(如騰訊雲API簽名、阿里雲APi簽名、亞馬遜API簽名等等)即可,無需從零開始設計一個API簽名方案。寫這篇文章的主要目的,是希望通過思考如何去設計一個可用API簽名的過程,更好地理解現有的各種大同小異的簽名方案背後的設計原理,從而更好地保護好我們的API介面。當然,有需要自己設計一個簽名方案的場景也可參考一下。

1、API簽名是什麼

API簽名可以理解為就是對API的調用進行簽名保護。是在進行API調用時,加了一個調用者及其調用行為的指紋資訊,以幫助服務端更好的識別用戶及其調用行為的合法性。其直接目的歸納為:

(1)明確調用者的身份(確認調用者是誰)

(2)明確調用者的調用行為(確認調用者想要做什麼)

圖·API簽名解決兩個問題

而明確調用者的身份和調用行為後,可基於調用者的身份做到包括但不限於以下幾點:

A:拒絕非法用戶身份者的調用請求

B:拒絕越權使用者的調用請求,保護隱私

C:控制訪問者的調用頻率,保護服務

D:記錄調用者的訪問記錄,以便追溯

……

由此可見,API簽名的真正目的是:通過明確調用者的身份,以便控制API的訪問許可權,從而保護數據的安全性

圖·API簽名的本質

2、如何設計一個API簽名

既然API簽名的目的是:明確調用者的身份及其調用行為,那麼我們進行設計時只有圍繞這兩點即可。

2.1、如何明確調用者

我們都知道,在程式的世界中,很難找到一個穩定且唯一的資訊去標識一個調用者,因為調用者本身的資訊(如IP、設備等)也是不固定的,所以,標識調用者最好的方法就是服務端統一分配:

2.1.1、用戶身份標識

(1)調用者調用API前,必須向系統申請一個唯一的標識

(2)系統為每個調用者分配一個唯一的ID,這裡暫定為SecretID

(3)調用者調用API時帶上該SecretID

(4)服務端 通過SecretID確認調用者身份

以上流程的問題,在於SecretID是明文顯示的,很容易被竊取和偽造;但SecretID又不能隱藏或加密,因為SecretID需要明確告訴服務端:我是誰?

所以,需要在SecretID之外,增加一個和SecretID綁定的資訊,我們稱之為:

2.1.2、用戶密鑰

用戶密鑰(即SecretKey)就是為了驗證用戶身份用的,為了提高其安全性能,必須保證

(1)調用者必須保護好SecretKey,不能在任何地方明文顯示

(2)SecretKey最好不在請求過程中傳輸

至於,密鑰如何分配、更換、失效、存儲等密鑰管理的內容不是本文重點,暫不深入。

那麼,問題來了,有了密鑰之後,如何驗證用戶的身份呢?這個就需要靠演算法來解決了。

圖·ID+Key確認用戶身份

2.1.3、簽名演算法選擇

在密碼學中,有對稱加密演算法、非對稱加密演算法、 希運算消息認證碼等等幾種方案可以很好保護用戶密鑰的同時,驗證用戶的身份。那麼,我們應該如何選擇呢?

(1)首先排除的是非對稱加密演算法,理由是耗時長,性能差。

通過實測,非對接加密演算法(RSA)相對加密演算法(AES)和 希運算消息認證碼演算法(HmacSHA256)的加解密耗時要高2~3個數量級,對於一個服務端來說,性能也是很重要的考慮標準,故一般不選擇非對稱加密演算法。

演算法類型

加密耗時

解密耗時

RSA

380317 ns/op

34427 ns/op

AES

885 ns/op

938 ns/op

HmacSHA256

1458 ns/op

1458 ns/op

(2)從以上結果看,Hmac和AES似乎都不錯,而且AES更優。但考慮到簽名的目的,除了明確用戶身份外,還要明確調用者的調用行為;也就是說,為了需要保證整個請求的完整性,需要加密整個請求的所有關鍵內容,這時,Hmac演算法的防偽造性(即修改一個位元組,簽名資訊就完全不一樣)的優勢就突顯出來了,在性能差不多的情況下,當然,選擇Hmac演算法了。

(3)Hmac支援的hash演算法非常多,但一般不建議使用MD5和SHA1,因其有哈希長度擴展攻擊(Hash Length Extension Attacks)的風險,故一般推薦使用HmacSHA256或HmacSHA512。

若服務端支援多種演算法,則請求時,需明確帶上使用的簽名方法:SignatureMethod

2.2、如何明確調用者的調用行為

方法很簡單,那就是把調用行為涉及的關鍵資訊都放到簽名內容中進行簽名。那麼,哪些是關鍵資訊呢?

2.2.1、請求的方法和介面

即每個請求Method和URL,這是每個請求都有的資訊,且最為關鍵的資訊。

2.2.2、請求的內容

請求內容一般指HEADERS、QueryString、BODY三大類。

那麼,哪一類內容需要添加的簽名內容中呢?

一個簡單的判斷標準,就是看這一塊的內容的變更是否影響請求結果,若影響了,一般要求加入到簽名內容中;若設計時還不確定,則全部內容加到簽名內容中即可。

備註:實際上,一般是哪個欄位有影響,添加哪個欄位最簡潔;但這樣的話,服務端就非常麻煩,需要對每個API介面的每個欄位分析,無論請求端還是服務端實現都特別麻煩且需要每個介面進行簽名聯調,不太現實。所以,一般是按大類進行的。

好了,到這裡,API簽名似乎已經完成了。

2.2.3、防重放

但是,對於部分請求來說,是有請求一次性要求,即同一請求內容一次和兩次的結果是不一樣的。這種情況下,惡意攻擊者,截取一個合法請求後,不停地使用該請求對服務端進行攻擊;這種攻擊可能造成

(1)如果該請求是寫請求,且服務端邏輯允許重複(如A向B轉1元),則會造成嚴重後果。

(2)如果該請求是非常耗時操作,則可能造成服務性能下降。

(3)如果是普通讀請求,看似無害,實則量大也是對後端服務性能的一種消化。

所以,在API簽名這裡,需要進行防重放設計,可以為後端其他服務減少壓力。實現的方法,也很簡單,那就是調用者每次調用時:

A:調用者生成並帶上一個隨機數Nonce

B:服務端該隨機數是否已出現,有則拒絕,無則存儲該隨機數並放過請求

這裡服務端要保證Nonce唯一,就得存儲已經用過的Nonce,但長期保持會帶來兩個問題

(1)存儲成本增加,日積月累,這裡要存儲的Nonce會越來越多,需要的存儲空間就越大

(2)碰撞概率增加,正常服務被拒絕概率增大;這裡隨著生成Nonce值越來越多,碰撞的概率一定越來越大,若通過增加Nonce值的長度,有增加存儲成本。

那麼,另一個可行的辦法,就是調用者每次請求時帶上當前請求時間點Timestamp,然後由服務端限制請求的時效性。

2.2.4、請求的時效性

即某個請求,其請求時間戳Timestamp,和服務端的當前時間在規定時間內(如1分鐘內)則為合法請求,反之,則視為無效請求。

如此,上面提到的Nonce值存儲成本可能比較大的問題,在結合Timestamp後,可大大降低存儲成本,如Timestamp=1min,則僅需存儲1min內的請求Nonce值即可,大大減少存儲的量級。

2.2.5、版本控制

另外,每個設計都很難做到完美,或者當前看已經比較完善,但隨著技術的發展,會逐漸的暴露一些缺陷,此時,想做一個可持續發展的API簽名方案,版本迭代自然少不了,所以,請求內容也可加上版本資訊。

3、API簽名方案實現

3.1、客戶端流程

圖·客戶端API簽名

(1)生成隨機數Nonce

(2)拼接簽名內容,生成簽名資訊

(3)調用API時,帶上簽名資訊

3.2、服務端流程

圖·服務端API簽名校驗流程

(1)校驗時間有效性Timestamp

(2)校驗Nonce唯一性

(3)提取SecretId和簽名資訊

(4)根據SecretId提取用戶密鑰SecretKey,用於生產簽名

(5)拼接簽名內容,生成簽名

(6)校驗請求籤名和服務端生產簽名是否一致

3.3、簽名生產流程

幾點聲明:

(1)以下示例簽名資訊可放在QueryString中,但簽名資訊也可放在包頭中

(2)為簡化流程,以下部分暫不考慮包頭簽名,原理相通,實現時,自己加上即可

(3)這裡的簽名演算法,指定為HmacSHA256

(4)拼接規則是多樣化的,這裡的各種拼接規則僅供參考

生成簽名串的大致過程如下:

圖·API簽名生成流程

假設調用者已經有了SecretID 和 SecretKey 分別是:

secretId: "SKIDz8krbsJ5yKBZQpn74WFkmLPx3EXAMPLE"  secretKey: "Gu5t9xGARNpq86cd98joQYCN3EXAMPLE"

3.3.1、確認是否做包體簽名

有包體,則做包體簽名,無包體,則不做包體簽名。以下請求包體格式規定為JSON,這裡的無需提取包體欄位進行拼接,直接對整個包體內容進行簽名即可,但對包體欄位到順序有要求。

假設本次要調用API介面名稱為:GetLibTypeList,POST方法,請求包體格式為JSON,請求包體欄位有PageIndex和PageSize。

(1)請求JSON包體轉換為字元串

假設,本次請求的Json包結構,如下所示

{      "PageIndex":0,      "PageSize":10  }

先將json包結構體進行json序列化成byte, 再將byte轉換為字元串,最終得到的包體簽名原字元串如下所示:

{"PageIndex":0,"PageSize":10}

(2)生成包體簽名串

首先使用簽名演算法HmacSHA256對上一步中獲得的 包體原文字元串 進行簽名,然後將生成的簽名串使用 Base64 進行編碼,即可獲得的包體簽名串。

UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD/X0s=

(3)簽名串編碼

生成的簽名串並不能直接作為請求參數,需要對其進行 URL 編碼。 ​ 如上一步使用HmacSHA256 生成的簽名串為UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD/X0s=,則其編碼後為

UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD%2FX0s%3D

因此,最終得到的簽名串請求參數 (Signature) 為:UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD%2FX0s%3D,它將用於生成最終的請求URL。

3.3.2、拼接請求字元串

請求參數主要有:

參數名稱

類型

示例值

Version

固定為:20191001

20191001

SecretId

密鑰ID

SKIDz8krbsJ5yKBZQpn74WFkmLPx3EXAMPLE

Timestamp

當前時間戳

1569490800

Nonce

隨機正整數

3557156860265374221

SignatureMethod

簽名方式

HmacSHA256或HmacSHA1

HashedRequestPayload

包體簽名字元串

UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD%2FX0s%3D

根據上述參數,使用HmacSHA256簽名方式拼接的請求字元串如下:

Version=20191001&SecretId=SKIDz8krbsJ5yKBZQpn74WFkmLPx3EXAMPLE&Timestamp=1569490800&Nonce=3557156860265374221&SignatureMethod=HmacSHA256&HashedRequestPayload=UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD%2FX0s%3D

備註:當無需包體簽名時,則不拼接HashedRequestPayload即可

3.3.3、拼接簽名原字元串

這裡規定簽名原文字元串的拼接規則為:

請求方法 + 請求主機 +請求路徑 + ? + 請求字元串

參數構成說明:

  • 請求方法: 即 POST 、GET等方法, 為保證簽名結果一致,一般需規定注意方法為全大寫。
  • 請求主機:即主機域名,此處是本地測試,則使用:localhost:8008,具體請以實際請求的域名為準。
  • 請求路徑: 即API 的請求路徑,本例中請求路徑為/GetLibTypeList
  • 請求字元串: 即上一步生成的請求字元串。

使用 HmacSHA256簽名方式拼接的簽名原字元串如下:

POSTlocalhost:8008/GetLibTypeList?Version=20191001&SecretId=SKIDz8krbsJ5yKBZQpn74WFkmLPx3EXAMPLE&Timestamp=1569490800&Nonce=3557156860265374221&SignatureMethod=HmacSHA256&HashedRequestPayload=UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD%2FX0s%3D

3.3.4、生成簽名串

使用簽名演算法HmacSHA256對上一步中獲得的 簽名原文字元串 進行簽名,然後將生成的簽名串使用 Base64 進行編碼,即可獲得請求籤名串如下所示:

+ysXvBSshSbHOsCX2zWBE1tapVs68hi5GLdcQtwBUNk=

生成的簽名串並不能直接作為請求參數,需要對其進行 URL 編碼,編碼後的簽名串如下所示:

%2BysXvBSshSbHOsCX2zWBE1tapVs68hi5GLdcQtwBUNk%3D

3.3.5、將簽名資訊添加到請求參數中

使用 HmacSHA256簽名方式,發送的POST請求URL如下所示:

http://localhost:8008/GetLibTypeList?Version=20191001&SecretId=SKIDz8krbsJ5yKBZQpn74WFkmLPx3EXAMPLE&Timestamp=1569490800&Nonce=3557156860265374221&SignatureMethod=HmacSHA256&HashedRequestPayload=UodgxU3P77iThrEJtsiHi2kjYJmNA2jGEgYNnMD%2FX0s%3D&Signature  =%2BysXvBSshSbHOsCX2zWBE1tapVs68hi5GLdcQtwBUNk%3D

注意:

(1)發送的請求URL各個參數無排序要求,但其順序必須和簽名原字元串的順序保持一致

(1)需規定簽名資訊Signature必須作為最後一個參數,拼接在最後面,以便截取

(2)所有請求參數的參數值均需要做 URL 編碼

需要注意的是,部分語言庫會自動對 URL 進行編碼,重複編碼會導致簽名校驗失敗。

4、程式碼實現

Go語言版實現程式碼可參考:https://github.com/esonlin/signature (含服務端程式碼和客戶端demo)