一.需求背景
在一些商業合作的場景下,合作方有自己的軟件系統並且具備開發能力,需要訪問我們的數據資源(比如:賬號、產品、統計等),一般的技術方案是提供HTTP API給合作方調用。此時為了保證數據的安全性以及對數據訪問范圍的控制,就必須驗證API調用方的身份,然后結合調用方的權限返回對應的資源,對於無法識別身份的調用方,服務端會進行攔截。
二.常用的API認證技術
2.1 App Secret Key + HMAC
這是一種用於給消息簽名的技術,我們怕消息在傳遞的過程中被人修改,所以,我們需要用對消息進行一個MAC算法,得到一個摘要字串,然后,接收方得到消息后,進行同樣的計算,然后比較這個MAC字符串,如果一致,則表明沒有被修改過(整個過程參看下圖)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技術完成這一工作,比如:SHA-256算法。
以SHA-256算法示例,簽名流程:
- 發送方以 Key 作為算法的簽名,對消息 Message 進行一個MAC算法,得到一個摘要字串 MAC。
- 接收方 接收消息 Message 后進行同樣的計算得到一個摘要字串 MAC。
- 接收方 然后比較這個 MAC 字符串是否一致,如果一致,則表明沒有被修改過。
2.2 OAuth 2.0
OAuth 是一個關於授權(authorization)的開放網絡標准,在全世界得到廣泛應用,目前的版本是2.0版。OAuth 2.0依賴於TLS/SSL的鏈路加密技術(HTTPS),完全放棄了簽名的方式,認證服務器再也不返回什么token secret的密鑰了。
2.2.1 Authorization Code Flow
Authorization Code 是最常使用的OAuth 2.0的授權許可類型,它適用於用戶給第三方應用授權訪問自己信息的場景。其流程圖如下:
授權流程:
- 當用戶 Resource Owner 訪問第三方應用 Client 的時候,第三方應用會把用戶帶到認證服務器 Authorization Server 上去。
- 當 Authorization Server 收到這個URL請求后,其會通過 client_id 來檢查 redirect_uri 和 scope 是否合法,如果合法,則彈出一個頁面,讓用戶授權。(如果用戶沒有登錄,則先讓用戶登錄,登錄完成后,出現授權訪問頁面)
- 當用戶授權同意訪問以后,Authorization Server 會跳轉回 Client ,並以返回一個 Authorization Code。
- 接下來,Client 就可以使用 Authorization Code 獲得 Access Token。
- 最后就是用 Access Token 請求 Resource Server 用戶的資源。
2.2.2 Client Credential Flow
客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進行認證。在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。
授權流程:
- Client 用自己的
client_id
和client_secret
向 Authorization Server 請求 Access Token。 - 然后 Client 使用Access Token訪問 Resource Server 相關的資源。
三.業內產品調研
3.1 微信支付
-
微信支付采用 App Secret Key + HMAC 簽名,首先介紹一下微信支付的大致原理:
-
微信是支付系統的開發方,掌管整個支付系統,負責記賬。
-
商家想要接入微信支付收銀,需要向微信支付部門申請商戶號。
-
普通用戶通過微信點擊商家的付款鏈接,進行付款。
-
微信后台記錄一筆用戶和商家之間的交易流水,然后通知商家系統支付成功。
好了,現在可以知道,交易過程其實就是商家系統和微信后台的接口互相調用,而且只需要單向的關注商家調用微信后台。
-
-
JSAPI支付-開發文檔,簽名算法:
假設傳遞的參數如下:
appid: wxd930ea5d5a258f4f mch_id: 10000100 device_info: 1000 body: test nonce_str: ibuaiVcKdpRxkhJA
第一步,設所有發送或者接收到的數據為集合M,將集合M內非空參數值的參數按照參數名ASCII碼從小到大排序(字典序),使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
第二步,在stringA最后拼接上key得到stringSignTemp字符串,並對stringSignTemp進行MD5運算,再將得到的字符串所有字符轉換為大寫,得到sign值signValue。
stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key為商戶平台設置的密鑰key sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5簽名方式 sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256簽名方式,部分語言的hmac方法生成結果二進制結果,需要調對應函數轉化為十六進制字符串。
最終發送的數據:
<xml> <appid>wxd930ea5d5a258f4f</appid> <mch_id>10000100</mch_id> <device_info>1000</device_info> <body>test</body> <nonce_str>ibuaiVcKdpRxkhJA</nonce_str> <sign>9A0A8659F005D6984697E2CA0A9CF3B7</sign> </xml>
3.2 微信公眾號
-
access_token是公眾號的全局唯一接口調用憑據,公眾號調用各接口時都需使用access_token。開發者需要進行妥善保存。access_token的存儲至少要保留512個字符空間。access_token的有效期目前為2個小時,需定時刷新,重復獲取將導致上次獲取的access_token失效。
-
接口調用請求說明
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
參數說明:
- grant_type:獲取access_token填寫client_credential
- appid:第三方用戶唯一憑證
- secret:第三方用戶唯一憑證密鑰,即appsecret
返回情況
正常情況下,微信會返回下述JSON數據包給公眾號:
{"access_token":"ACCESS_TOKEN","expires_in":7200}
參數說明:
- access_token:獲取到的憑證
- expires_in:憑證有效時間,單位:秒
3.3 微信網頁授權
-
如果用戶在微信客戶端中訪問第三方網頁,公眾號可以通過微信網頁授權機制,來獲取用戶基本信息,進而實現業務邏輯。
-
網頁授權AccessToken的流程
- 第一步:引導用戶進入授權頁面同意授權,獲取code
- 第二步:通過code換取網頁授權access_token
- 第三步:如果需要,開發者可以刷新網頁授權access_token,避免過期。
- 第四步:通過網頁授權access_token和openid獲取用戶基本信息
四、如何選擇HTTP API鑒權方案
4.1 HTTP API鑒權方式的對比
前面介紹幾種常用的API鑒權技術,在產品調研環節分別可以找到其落地場景。
首先拿 微信支付 來看,一筆交易的下單在一個接口中完成,請求的參數包含金額、商戶號等,都是非常關鍵的參數,必須要求嚴格校驗,防止被攻擊篡改,同時參數還有時效限制(含時間戳)。此時自然不適合使用OAuth 2.0的鑒權方式,AccessToken的請求不對參數進行校驗。
然后看下 微信公眾號 AccessToken的場景,可以看到使用AccessToken調用接口(管理公眾號菜單、管理賬號)都屬於一個企業范圍內的數據,可以這么理解,這部分信息屬於微信授權給企業的一份獨立資產,公眾號對應的企業有權限管理這份資產。此時使用AccessToken可以很好的控制訪問范圍。這里不是不能用 App Secret Key + HMAC 的鑒權方式,而是覺得這部分信息安全要求沒有支付高。另一方面,不對參數加密,通信也會更加高效(加密有耗時,比如文件上傳也不太適合進行加密)。
最后看下 微信網頁授權,同理類推,用戶的信息屬於每個獨立的用戶,獲取的AccessToken的訪問范圍也只能是當前用戶的信息。
4.2 HTTP API鑒權經驗分享
上面提到的兩種鑒權方式,無論是作為服務方還是調用方,我都在工作中都有使用到。個人覺得 App Secret Key + HMAC 實踐起來相對容易,客戶端對服務端的調用比較直接,鑒權不通過時可以通過接口的響應及時獲得反饋。
另一種,OAuth 2.0的AccessToken的方式,服務端需要維護AccessToken,並且還要控制AccessToken的失效,拿微信公眾號來看,新的AccessToken生成后,舊的AccessToken在5分鍾之內有效;客戶端需要維護一份AccessToken並及時刷新保持有效。再看下業務的交互上,比起 App Secret Key + HMAC 明顯多一些環節,環節多了就容易犯錯。
4.3 結論
最后,具體選擇使用哪一種鑒權方式,我想還是需要結合對應的業務場景來看。比如業務發展的初期,需要快速開發推向市場,這時就沒必要糾結,直接選擇一種相對而言簡單且不容易犯錯的 App Secret Key + HMAC 簽名鑒權。等到后續用戶量大了,業務成熟了,可以參考 微信公眾號、AWS s4簽名,精細划分每一個AccessToken的訪問范圍。
五.實踐-方案實現
實踐案例使用 App Secret Key + HMAC 的鑒權方式,下面會詳細介紹 客戶端簽名 和 服務端驗簽 的過程。
5.1 分配AppId和AppSecret
在簽名之前首先需要分配 AppId 和 AppSecret,落實到業務場景中,這個就是我們作為資源方分配給合作方的租戶配置。關於 AppId 和 AppSecret 的生成沒有標准規范,每家的生成算法都不一樣,也都不會公布出來。本次案例,我們使用32位的uuid作為AppId,以64位的hash串作為AppSecret:
// 生成AppId
private static String generateAppId() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replaceAll("-", "");
}
// 生成AppSecret
private static String generateAppSecret() {
UUID uuid = UUID.randomUUID();
return DigestUtils.sha256Hex(uuid.toString());
}
計算得出:
APPID = "ivv49q404zfp8075ivbcwye4ardqafha"
APP_SECRET = "ut338c829x2yzfnklvy8lezyu3ndsss68dyzo9opt3icbin7lv7p2j4b0i2cvjz8"
5.2 客戶端簽名
-
假設傳遞的參數如下:
private static final String APPID = "ivv49q404zfp8075ivbcwye4ardqafha"; /** * 下單請求對象 */ class PlaceOrderForm { String appid; Integer totalAmount; String body; String detail; String nonceStr; } /** * 模擬請求對象 */ private static PlaceOrderForm mockWebForm () { PlaceOrderForm form = new PlaceOrderForm(); form.appid = APPID; form.body = "test"; form.detail = "test"; form.nonceStr = "123456"; form.totalAmount = 88; return form; }
-
第一步,設所有發送或者接收到的數據為集合M,將集合M內非空參數值的參數按照參數名ASCII碼從小到大排序(字典序),使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字符串stringA。
/** * TreeMap會根據Key排序 */ private static Map<String, String> confirmToMap(PlaceOrderForm form) throws Exception { Map<String, String> map = new TreeMap<>(); Field[] fields = PlaceOrderForm.class.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); Object value = field.get(form); if (value != null && !field.getName().equals("sign")) { if (value instanceof String) { map.put(field.getName(), (String) value); } else if (value instanceof Integer) { map.put(field.getName(), String.valueOf(value)); } } } return map; }
-
第二步,在stringA最后拼接上appsecret得到stringSignTemp字符串,並對stringSignTemp進行MD5運算,再將得到的字符串所有字符轉換為大寫,得到sign值signValue。
private static final String APP_SECRET = "ut338c829x2yzfnklvy8lezyu3ndsss68dyzo9opt3icbin7lv7p2j4b0i2cvjz8"; /** * 生成簽名 */ private static String sign(Map<String, String> params) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(entry.getKey()); sb.append("="); sb.append(entry.getValue()); sb.append("&"); } sb.append("appsecret="); sb.append(APP_SECRET); return DigestUtils.md5Hex(sb.toString()).toUpperCase(); }
-
最后計算得到摘要
public static void main(String[] args) { PlaceOrderForm form = mockWebForm(); try { Map<String, String> stringStringMap = confirmToMap(form); String sign = sign(stringStringMap); System.out.println(sign); } catch (Exception e) { e.printStackTrace(); } }
5.3 服務端驗簽
服務端接收請求的參數,使用同樣的簽名算法計算出摘要 sign 進行比較,如果一致,則說明請求沒有被修改。