安全的API 介面 解決方案
開發中經常用到介面,尤其是在面向服務的soa架構中,資料互動全是用的介面。
幾年以前我認為,我寫個介面,不向任何人告知我的介面地址,我的介面就是安全的,現在回想真是too young,too simple。但凡部署在廣域網的應用程式,隨隨便便的好多工具可以根據ip或域名掃描應用程式的所有暴露的介面,進而分析引數,注入程式,分分鐘被攻擊。
那咋才能保證介面的安全性呢?
(一)面臨的主要安全問題
a.網路環境假設:
a1.假設公共網路(Internet,如:WIFI、非家庭網路、非辦公網路等) 是不安全的,一切基於HTTP協議的請求/響應(Request or Response)都是可以被截獲的、篡改、重放(重發)的。
b.介面安全要求:
b1.防偽裝攻擊(案例:在公共網路環境中,第三方 有意或惡意 的呼叫我們的介面)
b2.防篡改攻擊(案例:在公共網路環境中,請求頭/查詢字串/內容 在傳輸過程被修改)
b3.防重放攻擊(案例:在公共網路環境中,請求被截獲,稍後被重放或多次重放)
b4.防資料資訊洩漏(案例:截獲使用者登入請求,截獲到賬號、密碼等)
(二)可參考的商業標準
可參見: HTTP資料傳輸安全方案 HTTPS(HTTP安全商業標準)
http://baike.baidu.com/view/14121.htm
(三)可參考國內網際網路廠商參考
新浪OpenAPI、騰訊、淘寶 等。
(四)設計原則
1.輕量級
2.適合於異構系統(跨作業系統、多語言簡易實現)
3.易於開發
4.易於測試
5.易於部署
6.滿足介面安全需求(滿足b1 b2 b3要求),無過度設計。
其它:介面安全要求b4部分,主要針對目前使用者中心的登入介面
設計原則是:使用HTTPS安全協議 或 傳輸內容使用非對稱加密,目前我們採用的後者。
(五)適用範圍
1.所有寫操作介面(增、刪、改 操作)
2.非公開的讀介面(如:涉密/敏感/隱私 等資訊)
(六)介面引數簽名 實現思路參考
必要的輸入引數
引數名 |
型別 |
必選 |
描述 |
_appid | string | 是 | 呼叫方身份ID,介面提供方用此來識別調不同的呼叫者,該引數是API基本規範的一部分,請詳見API公共規範。 |
_sign |
string |
是 |
一次介面呼叫的簽名值,伺服器端 “防止 偽裝請求/防篡改/ 防重發” 識別的重要依據。 |
_timestamp |
Int |
是 |
時間戳(long Timestamp = DateTime.Now.Ticks;) |
簽名演算法過程:
1.對除簽名外的所有請求引數按key做的升序排列,value無需編碼。
(假設當前時間的時間戳是12345678)
例如:有c=3,b=2,a=1 三個參,另加上時間戳後, 按key排序後為:a=1,b=2,c=3,_timestamp=12345678。
2 把引數名和引數值連線成字串,得到拼裝字元:a1b2c3_timestamp12345678
3 用申請到的appkey 連線到接拼裝字串頭部和尾部,然後進行32位MD5加密,最後將到得MD5加密摘要轉化成大寫。
示例:假設appkey=test,md5(testa1b2c3_timestamp12345678test),取得MD5摘要值 C5F3EB5D7DC2748AED89E90AF00081E6 。
再看一個更具體的Sample Code:
如何得取如下請求的簽名值:
http://api.demo.com/dog/add?1=壹&A=aaa&Z=zzz&_appid=club&_timestamp=12345678&a=AAA&z=ZZZ
C#實現程式碼如下 ( 請新建一個C#程式碼檔案 SampleCode.cs ):
test()方法展示瞭如何取得該請的簽名引數值 ( _sign=8B0E081689789CF66490E65BB8E1B0E7 ),現實業務中依據自己的情況,把建立請求的過程封裝成公共方法,使得請求url的建立過程對開發人員透明,簡化處理。
[下方會提供 .Net的 Sample Code]
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
.Net
-
using System;
-
using System.Collections.Generic;
-
using System.Linq;
-
using System.Text;
-
namespace test3
-
{
-
public class SampleCode
-
{
-
public static string test()
-
{
-
int _timestamp = 12345678;
-
var param = new SortedDictionary<string, string>(new AsciiComparer());
-
param.Add("z", "ZZZ");
-
param.Add("a", "AAA");
-
param.Add("Z", "zzz");
-
param.Add("A", "aaa");
-
param.Add("2", "貳");
-
param.Add("1", "壹");
-
param.Add("_appid", "club");
-
param.Add("_timestamp", _timestamp.ToString());
-
string _sign = GetSign(param);
-
string urlParam = string.Join("&", param.Select(i => i.Key + "=" + i.Value));
-
string url = "http://api.demo.com/dog/add?" + urlParam + "&_sign=" + _sign;
-
return url;
-
}
-
public static string GetSign(SortedDictionary<string, string> paramList, string appKey = "test")
-
{
-
paramList.Remove("_sign");
-
StringBuilder sb = new StringBuilder(appKey);
-
foreach (var p in paramList)
-
sb.Append(p.Key).Append(p.Value);
-
sb.Append(appKey);
-
return GetMD5(sb.ToString());
-
}
-
public static string GetMD5(string str)
-
{
-
if (string.IsNullOrEmpty(str))
-
return str;
-
var sb = new StringBuilder(32);
-
var md5 = System.Security.Cryptography.MD5.Create();
-
var output = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
-
for (int i = 0; i < output.Length; i++)
-
sb.Append(output[i].ToString("X").PadLeft(2, '0'));
-
return sb.ToString();
-
}
-
}
-
/// <summary>
-
/// 基於ASCII碼排序規則的String比較器
-
/// Author:HeDaHong
-
/// </summary>
-
public class AsciiComparer : System.Collections.Generic.IComparer<string>
-
{
-
public int Compare(string a, string b)
-
{
-
if (a == b)
-
return 0;
-
else if (string.IsNullOrEmpty(a))
-
return -1;
-
else if (string.IsNullOrEmpty(b))
-
return 1;
-
if (a.Length <= b.Length)
-
{
-
for (int i = 0; i < a.Length; i++)
-
{
-
if (a[i] < b[i])
-
return -1;
-
else if (a[i] > b[i])
-
return 1;
-
else
-
continue;
-
}
-
return a.Length == b.Length ? 0 : -1;
-
}
-
else
-
{
-
for (int i = 0; i < b.Length; i++)
-
{
-
if (a[i] < b[i])
-
return -1;
-
else if (a[i] > b[i])
-
return 1;
-
else
-
continue;
-
}
-
return 1;
-
}
-
}
-
}
-
}
總結:
- 介面呼叫方和介面提供方約定好統一的引數加密演算法
- 介面呼叫方在呼叫時把加密後的_sign放在引數中去請求介面
- 介面提供方接到響應後,判斷時間戳是不是在有效時間內(這個時間間隔根據你的安全範圍可以是10分鐘,5分鐘,20秒等,過期失效,前提是需要保證介面提供方和呼叫方的伺服器時間為準確的網路同步時間)
- 把引數中除了_sign以外的引數進行加密,然後把加密結果和傳過來的_sign比較,相同則執行呼叫請求。
1.完全開放的介面
有沒有這樣的介面,誰都可以呼叫,誰都可以訪問,不受時間空間限制,只要能連上網際網路就能呼叫,毫無安全可言。
實話說,這樣的介面我們天天都在接觸,你查快遞,你查天氣預報,你查飛機,火車班次等,這些都是有公共的介面。
我把這稱之為裸奔時代。程式碼如下:
/// <summary> /// 介面對外公開 /// </summary> /// <returns></returns> [HttpGet] [Route("NoSecure")] public HttpResponseMessage NoSecure(int age) { var result = new ResultModel<object>() { ReturnCode = 0, Message = string.Empty, Result = string.Empty }; var dataResult = stulist.Where(T => T.Age == age).ToList(); result.Result = dataResult; return GetHttpResponseMessage(result); }
2.介面引數加密(基礎加密)
你寫個介面,你只想讓特定的呼叫方使用,你把這些呼叫的人叫到一個小屋子,給他們宣佈說我這裡有個介面只打算給你們用,我給你們每人一把鑰匙,你們用的時候拿著這把鑰匙即可。
這把鑰匙就是我上文說到的引數加密規則,有了這個規則就能呼叫。
這有安全問題啊,這裡面的某個成員如果哪個不小心丟了鑰匙或者被人竊取,掌握鑰匙的人是不是也可以來掉用介面了呢?而且他可以複製很多鑰匙給不明不白的人用。
相當於有人拿到了你的請求連結,如果業務沒有對連結唯一性做判斷(實際上業務邏輯通常不會把每次請求的加密簽名記錄下來,所以不會做唯一性判斷),就會被重複呼叫,有一定安全漏洞,怎麼破?先看這個場景的程式碼,然後繼續往下看!
/// <summary> /// 介面加密 /// </summary> /// <returns></returns> [HttpGet] [Route("SecureBySign")] public HttpResponseMessage SecureBySign([FromUri]int age, long _timestamp, string appKey, string _sign) { var result = new ResultModel<object>() { ReturnCode = 0, Message = string.Empty, Result = string.Empty }; #region 校驗簽名是否合法 var param = new SortedDictionary<string, string>(new AsciiComparer()); param.Add("age", age.ToString()); param.Add("appKey", appKey); param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign) { result.ReturnCode = -2; result.Message = "簽名不合法"; return GetHttpResponseMessage(result); } #endregion var dataResult = stulist.Where(T => T.Age == age).ToList(); result.Result = dataResult; return GetHttpResponseMessage(result); }
3.介面引數加密+介面時效性驗證(一般達到這個級別已經非常安全了)
繼上一步,你發現有不明不白的人呼叫你的介面,你很不爽,隨即把真正需要呼叫介面的人又叫來,告訴他們每天給他們換一把鑰匙。和往常一樣,有個別夥伴的鑰匙被小偷偷走了,小偷煞費苦心,經過數天的踩點觀察,準備在一個月黑風高的夜晚動手。拿出鑰匙,搗鼓了半天也無法開啟你的神聖之門,因為小偷不知道你天天都在換新鑰匙。
小偷不服,經過一段時間琢磨,小偷發現了你們換鑰匙的規律。在一次獲得鑰匙之後,不加思索,當天就動手了,因為他知道他手裡的鑰匙在第二天你更換鑰匙後就失效了。
結果,小偷如願。怎麼破?先看這個場景的程式碼,然後繼續往下看!
/// <summary> /// 介面加密並根據時間戳判斷有效性 /// </summary> /// <returns></returns> [HttpGet] [Route("SecureBySign/Expired")] public HttpResponseMessage SecureBySign_Expired([FromUri]int age, long _timestamp, string appKey, string _sign) { var result = new ResultModel<object>() { ReturnCode = 0, Message = string.Empty, Result = string.Empty }; #region 判斷請求是否過期---假設過期時間是20秒 DateTime requestTime = GetDateTimeByTicks(_timestamp); if (requestTime.AddSeconds(20) < DateTime.Now) { result.ReturnCode = -1; result.Message = "介面過期"; return GetHttpResponseMessage(result); } #endregion #region 校驗簽名是否合法 var param = new SortedDictionary<string, string>(new AsciiComparer()); param.Add("age", age.ToString()); param.Add("appKey", appKey); param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign) { result.ReturnCode = -2; result.Message = "簽名不合法"; return GetHttpResponseMessage(result); } #endregion var dataResult = stulist.Where(T => T.Age == age).ToList(); result.Result = dataResult; return GetHttpResponseMessage(result); }
4.介面引數加密+時效性驗證+私鑰(達到這個級別安全性固若金湯)
繼上一步,你發現道高一尺魔高一丈,仍然有偷盜事情發生。咋辦呢?你打算下血本,給每個人配一把鑰匙的基礎上,再給每個人發個暗號,即使鑰匙被小偷弄去了,小偷沒有暗號,任然無法如願,而且這樣很容易定位是誰的暗號洩漏問題,找到問題根源,只需要給當前這個人換下鑰匙就行了,不用大動干戈。
但這個並不是萬無一失的,因為鑰匙畢竟還有可能被小偷搞到。程式碼如下:
/// <summary> /// 介面加密並根據時間戳判斷有效性而且帶著私有key校驗 /// </summary> /// <returns></returns> [HttpGet] [Route("SecureBySign/Expired/KeySecret")] public HttpResponseMessage SecureBySign_Expired_KeySecret([FromUri]int age, long _timestamp, string appKey, string _sign) { //key集合,這裡隨便弄兩個測試資料 //如果呼叫方比較多,需要稽核授權,根據一定的規則生成key把這些資料存放在資料庫中,如果功能擴充套件開來,可以針對不同的呼叫方做不同的功能許可權管理 //在呼叫介面時動態從庫裡取,每個呼叫方在呼叫時帶上他的key,呼叫方一般把自己的key放到網站配置中 Dictionary<string, string> keySecretDic = new Dictionary<string, string>(); keySecretDic.Add("key_zhangsan", "D9U7YY5D7FF2748AED89E90HJ88881E6");//張三的key, keySecretDic.Add("key_lisi", "I9O6ZZ3D7FF2748AED89E90ZB7732M9");//李四的key var result = new ResultModel<object>() { ReturnCode = 0, Message = string.Empty, Result = string.Empty }; #region 判斷請求是否過期---假設過期時間是20秒 DateTime requestTime = GetDateTimeByTicks(_timestamp); if (requestTime.AddSeconds(20) < DateTime.Now) { result.ReturnCode = -1; result.Message = "介面過期"; return GetHttpResponseMessage(result); } #endregion #region 根據appkey獲取key值 string secret = keySecretDic.Where(T => T.Key == appKey).FirstOrDefault().Value; #endregion #region 校驗簽名是否合法 var param = new SortedDictionary<string, string>(new AsciiComparer()); param.Add("age", age.ToString()); param.Add("appKey", appKey); param.Add("appSecret", secret);//把secret加入進行加密 param.Add("_timestamp", _timestamp.ToString()); string currentSign = SignHelper.GetSign(param, appKey); if (_sign != currentSign) { result.ReturnCode = -2; result.Message = "簽名不合法"; return GetHttpResponseMessage(result); } #endregion var dataResult = stulist.Where(T => T.Age == age).ToList(); result.Result = dataResult; return GetHttpResponseMessage(result); }
5.介面引數加密+時效性驗證+私鑰+Https(我把這個級別稱之為金鐘罩,世間最安全莫過於此)
繼上一步,我們給傳輸機制改為Https,這下小偷徹底懵逼了。那麼問題來了,Https咋玩兒呢?可以在本地搭個環境,參考此文:http://www.cnblogs.com/naniannayue/archive/2012/11/19/2776948.html
另:本文的介面是用的MVC WebAPI寫的,完全基於RESTful標準。如對此不是特別瞭解可以參考此文:http://www.cnblogs.com/landeanfen/p/5501490.html
注:demo不能直接執行,需要把兩個web專案配置到iis中,api代表介面提供方,他的主域需要配置到business的webconfig中,在瀏覽器位址列分別請求business中的各個呼叫介面方法來實現介面呼叫。
1、如果想驗證引數錯誤,需要在請求介面時打個斷點把介面url取出,篡改url引數,然後在瀏覽器中模擬請求
2、如果想驗證介面超時,需要在請求介面時打個斷點把介面url取出,然後等到了超時時間,然後在瀏覽器中模擬請求
3、如果想驗證私鑰錯誤,需要在請求介面時打個斷點把介面url取出,然後修改business的私鑰配置,然後在瀏覽器中模擬請求