接口的安全性主要圍繞Token、Timestamp和Sign三個機制展開設計,保證接口的數據不會被篡改和重復調用。
主要有以下幾個部分:
- Token授權機制
Token授權機制:用戶使用用戶名密碼登錄后服務器給客戶端返回一個Token(通常是UUID),並將Token-UserId以鍵值對的形式存放在緩存服務器中。服務端接收到請求后進行Token驗證,如果Token不存在,說明請求無效。Token是客戶端訪問服務端的憑證。
- 時間戳超時機制
時間戳超時機制:用戶每次請求都帶上當前時間的時間戳timestamp,服務端接收到timestamp后跟當前時間進行比對,如果時間差大於一定時間(比如5分鍾),則認為該請求失效。時間戳超時機制是防御DOS攻擊的有效手段。
- 簽名機制
簽名機制:將 Token 和 時間戳 加上其他請求參數再用MD5或SHA-1算法(可根據情況加點鹽)加密,加密后的數據就是本次請求的簽名sign,服務端接收到請求后以同樣的算法得到簽名,並跟當前的簽名進行比對,如果不一樣,說明參數被更改過,直接返回錯誤標識。簽名機制保證了數據不會被篡改。
拒絕重復調用(非必須):客戶端第一次訪問時,將簽名sign存放到緩存服務器中,超時時間設定為跟時間戳的超時時間一致,二者時間一致可以保證無論在timestamp限定時間內還是外 URL都只能訪問一次。如果有人使用同一個URL再次訪問,如果發現緩存服務器中已經存在了本次簽名,則拒絕服務。如果在緩存中的簽名失效的情況下,有人使用同一個URL再次訪問,則會被時間戳超時機制攔截。這就是為什么要求時間戳的超時時間要設定為跟時間戳的超時時間一致。拒絕重復調用機制確保URL被別人截獲了也無法使用(如抓取數據)。
整個流程如下:
1、客戶端通過用戶名密碼登錄服務器並獲取Token
2、客戶端生成時間戳timestamp,並將timestamp作為其中一個參數
3、客戶端將所有的參數,包括Token和timestamp按照自己的算法進行排序加密得到簽名sign
4、將token、timestamp和sign作為請求時必須攜帶的參數加在每個請求的URL后邊(http://url/request?token=123×tamp=123&sign=123123123)
5、服務端寫一個過濾器對token、timestamp和sign進行驗證,只有在token有效、timestamp未超時、緩存服務器中不存在sign三種情況同時滿足,本次請求才有效
在以上三中機制的保護下,
如果有人劫持了請求,並對請求中的參數進行了修改,簽名就無法通過;
如果有人使用已經劫持的URL進行DOS攻擊,服務器則會因為緩存服務器中已經存在簽名或時間戳超時而拒絕服務,所以DOS攻擊也是不可能的;
如果簽名算法和用戶名密碼都暴露了,那真的需要開光了吧。。。。
實際項目中需要根據業務情況作出裁剪,比如可以只使用簽名機制就可以保證信息不會被篡改,或者定向提供服務的時候只用Token機制就可以了。
寫出安全接口
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咋玩兒呢?