WebApi安全性 使用TOKEN+簽名驗證
首先問大家一個問題,你在寫開放的API接口時是如何保證數據的安全性的?先來看看有哪些安全性問題在開放的api接口中,我們通過http Post或者Get方式請求服務器的時候,會面臨著許多的安全性問題,例如:
- 請求來源(身份)是否合法?
- 請求參數被篡改?
- 請求的唯一性(不可復制),防止請求被惡意攻擊
為了保證數據在通信時的安全性,我們可以采用TOKEN+參數簽名的方式來進行相關驗證。
比如說我們客戶端需要查詢產品信息這個操作來進行分析,客戶端點擊查詢按鈕==》調用服務器端api進行查詢==》服務器端返回查詢結果
一、不進行驗證的方式
api查詢接口:
客戶端調用:http://api.XXX.com/getproduct?id=value1
如上,這種方式簡單粗暴,在瀏覽器直接輸入"http://api.XXX.com/getproduct?id=value1",即可獲取產品列表信息了,但是這樣的方式會存在很嚴重的安全性問題,沒有進行任何的驗證,大家都可以通過這個方法獲取到產品列表,導致產品信息泄露。
那麽,如何驗證調用者身份呢?如何防止參數被篡改呢?如何保證請求的唯一性? 如何保證請求的唯一性,防止請求被惡意攻擊呢?
二、使用TOKEN+簽名認證 保證請求安全性
token+簽名認證的主要原理是:1.做一個認證服務,提供一個認證的webapi,用戶先訪問它獲取對應的token
2.用戶拿著相應的token以及請求的參數和服務器端提供的簽名算法計算出簽名後再去訪問指定的api
3.服務器端每次接收到請求就獲取對應用戶的token和請求參數,服務器端再次計算簽名和客戶端簽名做對比,如果驗證通過則正常訪問相應的api,驗證失敗則返回具體的失敗信息
具體代碼如下 :
1.用戶請求認證服務GetToken,將TOKEN保存在服務器端緩存中,並返回對應的TOKEN到客戶端(該請求不需要進行簽名認證)
public HttpResponseMessage GetToken(string staffId) { ResultMsg resultMsg = null; int id = 0; //判斷參數是否合法 if (string.IsNullOrEmpty(staffId) || (!int.TryParse(staffId, out id))) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError; resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText(); resultMsg.Data = ""; return HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); } //插入緩存 Token token =(Token)HttpRuntime.Cache.Get(id.ToString()); if (HttpRuntime.Cache.Get(id.ToString()) == null) { token = new Token(); token.StaffId = id; token.SignToken = Guid.NewGuid(); token.ExpireTime = DateTime.Now.AddDays(1); HttpRuntime.Cache.Insert(token.StaffId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero); } //返回token信息 resultMsg =new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.Success; resultMsg.Info = ""; resultMsg.Data = token; return HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); }
2.客戶端調用服務器端API,需要對請求進行簽名認證,簽名方式如下
(1) get請求:按照請求參數名稱將所有請求參數按照字母先後順序排序得到:keyvaluekeyvalue...keyvalue 字符串如:將arong=1,mrong=2,crong=3 排序為:arong=1, crong=3,mrong=2 然後將參數名和參數值進行拼接得到參數字符串:arong1crong3mrong2。
public static Tuple<string,string> GetQueryString(Dictionary<string, string> parames) { // 第一步:把字典按Key的字母順序排序 IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parames); IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); // 第二步:把所有參數名和參數值串在一起 StringBuilder query = new StringBuilder(""); //簽名字符串 StringBuilder queryStr = new StringBuilder(""); //url參數 if (parames == null || parames.Count == 0) return new Tuple<string,string>("",""); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); queryStr.Append("&").Append(key).Append("=").Append(value); } } return new Tuple<string, string>(query.ToString(), queryStr.ToString().Substring(1, queryStr.Length - 1)); }
post請求:將請求的參數對象序列化為json格式字符串
Product product = new Product() { Id = 1, Name = "安慕希", Count = 10, Price = 58.8 }; var data=JsonConvert.SerializeObject(product);
(2)在請求頭中添加timespan(時間戳),nonce(隨機數),staffId(用戶Id),signature(簽名參數)
//加入頭信息 request.Headers.Add("staffid", staffId.ToString()); //當前請求用戶StaffId request.Headers.Add("timestamp", timeStamp); //發起請求時的時間戳(單位:毫秒) request.Headers.Add("nonce", nonce); //發起請求時的時間戳(單位:毫秒) request.Headers.Add("signature", GetSignature(timeStamp,nonce,staffId,data)); //當前請求內容的數字簽名
(3)根據請求參數計算本次請求的簽名,用timespan+nonc+staffId+token+data(請求參數字符串)得到signStr簽名字符串,然後再進行排序和MD5加密得到最終的signature簽名字符串,添加到請求頭中
private static string GetSignature(string timeStamp,string nonce,int staffId,string data) { Token token = null; var resultMsg = GetSignToken(staffId); if (resultMsg != null) { if (resultMsg.StatusCode == (int)StatusCodeEnum.Success) { token = resultMsg.Result; } else { throw new Exception(resultMsg.Data.ToString()); } } else { throw new Exception("token為null,員工編號為:" +staffId); } var hash = System.Security.Cryptography.MD5.Create(); //拼接簽名數據 var signStr = timeStamp +nonce+ staffId + token.SignToken.ToString() + data; //將字符串中字符按升序排序 var sortStr = string.Concat(signStr.OrderBy(c => c)); var bytes = Encoding.UTF8.GetBytes(sortStr); //使用MD5加密 var md5Val = hash.ComputeHash(bytes); //把二進制轉化為大寫的十六進制 StringBuilder result = new StringBuilder(); foreach (var c in md5Val) { result.Append(c.ToString("X2")); } return result.ToString().ToUpper(); }
(4) webapi接收到相應的請求,取出請求頭中的timespan,nonc,staffid,signature 數據,根據timespan判斷此次請求是否失效,根據staffid取出相應token判斷token是否失效,根據請求類型取出對應的請求參數,然後服務器端按照同樣的規則重新計算請求簽名,判斷和請求頭中的signature數據是否相同,如果相同的話則是合法請求,正常返回數據,如果不相同的話,該請求可能被惡意篡改,禁止訪問相應的數據,返回相應的錯誤信息
如下使用全局過濾器攔截所有api請求進行統一的處理
public class ApiSecurityFilter : ActionFilterAttribute { public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) { ResultMsg resultMsg = null; var request = actionContext.Request; string method = request.Method.Method; string staffid = String.Empty, timestamp = string.Empty, nonce = string.Empty, signature = string.Empty; int id = 0; if (request.Headers.Contains("staffid")) { staffid = HttpUtility.UrlDecode(request.Headers.GetValues("staffid").FirstOrDefault()); } if (request.Headers.Contains("timestamp")) { timestamp = HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault()); } if (request.Headers.Contains("nonce")) { nonce = HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault()); } if (request.Headers.Contains("signature")) { signature = HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault()); } //GetToken方法不需要進行簽名驗證 if (actionContext.ActionDescriptor.ActionName == "GetToken") { if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce))) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError; resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } else { base.OnActionExecuting(actionContext); return; } } //判斷請求頭是否包含以下參數 if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError; resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } //判斷timespan是否有效 double ts1 = 0; double ts2 = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds; bool timespanvalidate = double.TryParse(timestamp, out ts1); double ts = ts2 - ts1; bool falg = ts > int.Parse(WebSettingsConfig.UrlExpireTime) * 1000; if (falg || (!timespanvalidate)) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.URLExpireError; resultMsg.Info = StatusCodeEnum.URLExpireError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } //判斷token是否有效 Token token = (Token)HttpRuntime.Cache.Get(id.ToString()); string signtoken = string.Empty; if (HttpRuntime.Cache.Get(id.ToString()) == null) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.TokenInvalid; resultMsg.Info = StatusCodeEnum.TokenInvalid.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } else { signtoken = token.SignToken.ToString(); } //根據請求類型拼接參數 NameValueCollection form = HttpContext.Current.Request.QueryString; string data = string.Empty; switch (method) { case "POST": Stream stream = HttpContext.Current.Request.InputStream; string responseJson = string.Empty; StreamReader streamReader = new StreamReader(stream); data = streamReader.ReadToEnd(); break; case "GET": //第一步:取出所有get參數 IDictionary<string, string> parameters = new Dictionary<string, string>(); for (int f = 0; f < form.Count; f++) { string key = form.Keys[f]; parameters.Add(key, form[key]); } // 第二步:把字典按Key的字母順序排序 IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters); IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); // 第三步:把所有參數名和參數值串在一起 StringBuilder query = new StringBuilder(); while (dem.MoveNext()) { string key = dem.Current.Key; string value = dem.Current.Value; if (!string.IsNullOrEmpty(key)) { query.Append(key).Append(value); } } data = query.ToString(); break; default: resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.HttpMehtodError; resultMsg.Info = StatusCodeEnum.HttpMehtodError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } bool result = SignExtension.Validate(timestamp, nonce, id, signtoken,data, signature); if (!result) { resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.HttpRequestError; resultMsg.Info = StatusCodeEnum.HttpRequestError.GetEnumText(); resultMsg.Data = ""; actionContext.Response = HttpResponseExtension.toJson(JsonConvert.SerializeObject(resultMsg)); base.OnActionExecuting(actionContext); return; } else { base.OnActionExecuting(actionContext); } } public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { base.OnActionExecuted(actionExecutedContext); } }
然後我們進行測試,檢驗api請求的合法性
Get請求:
1.獲取產品數據,傳遞參數id=1,name="wahaha" ,完整請求為http://localhost:14826/api/product/getproduct?id=1&name=wahaha
2.請求頭添加timespan,staffid,nonce,signature字段
3.如圖當data裏面的值為id1namewahaha的時候請求頭中的signature和服務器端計算出來的result的值是完全一樣的,當我將data修改為id1namewahaha1之後,服務器端計算出來的簽名result和請求頭中提交的signature就不相同了,就表示為不合法的請求了
4.不合法的請求就會被識別為請求參數已被修改
合法的請求則會返回對應的商品信息
post請求:
1.post對象序列化為json字符串後提交到後臺,後臺返回相應產品信息
2.後臺獲取請求的參數信息
3.判斷簽名是否成功,第一次請求簽名參數signature和服務器端計算result完全相同, 然後當把請求參數中count的數量從10改成100之後服務器端計算的result和請求簽名參數signature不同,所以請求不合法,是非法請求,同理如果其他任何參數被修改最後計算的結果都會和簽名參數不同,請求同樣識別為不合法請求
總結:
通過上面的案例,我們可以看出,安全的關鍵在於參與簽名的TOKEN,整個過程中TOKEN是不參與通信的,所以只要保證TOKEN不泄露,請求就不會被偽造。
然後我們通過timestamp時間戳用來驗證請求是否過期,這樣就算被人拿走完整的請求鏈接也是無效的。
Sign簽名的方式能夠在一定程度上防止信息被篡改和偽造,保障通信的安全
源碼地址:https://github.com/13138899620/TokenSign
WebApi安全性 使用TOKEN+簽名驗證