記錄下Webapi簽名機制
首先,寫這篇文章的原因是因為最近某一個項目中的接口被人為調用了,導致了數據庫數據被串改。雖然是內部人無意點的,但還是引起了我的擔憂,所有整理了下關於Webapi的相關簽名機制。
一、我們在開發接口時,有時候嫌麻煩就懶進行相關的驗證或只進行一些簡單的驗證,這樣客戶端就可以直接調用:如
調用Webapi接口:http://XXX.XXX.XX.XXX:8123/Token/GetTest?ID=123456
這種方式簡單粗暴,在瀏覽器直接輸入"http://XXX.XXX.XX.XXX:8123/Token/GetTest?ID=123456",即可獲取產品列表信息了,但是這樣的方式會存在很嚴重的安全性問題,沒有進行任何的驗證,大家都可以通過這個方法獲取到產品列表,導致產品信息泄露,下面簡單記錄下使用使用TOKEN+簽名認證
二、使用TOKEN+簽名認證 保證請求安全性
token+簽名認證的主要原理是:1.做一個認證服務,提供一個認證的webapi,用戶先訪問它獲取對應的token
2.用戶拿著相應的token以及請求的參數和服務器端提供的簽名算法計算出簽名後再去訪問指定的api
3.服務器端每次接收到請求就獲取對應用戶的token和請求參數,服務器端再次計算簽名和客戶端簽名做對比,如果驗證通過則正常訪問相應的api,驗證失敗則 返回具體的失敗信息
具體代碼如下:
1.用戶請求認證服務GetToken,將token保存在服務器端緩存中,並返回對應的Token到客戶端(該請求不需要進行簽名認證),使用GET調用方式
[HttpGet] public IHttpActionResult GetToken(string signKey) { if (string.IsNullOrEmpty(signKey)) return Json<ResultMsg>(new ResultMsg((int)ExceptionStatus.ParameterError, EnumExtension.GetEnumText(ExceptionStatus.ParameterError), null)); //根據簽名ID獲取緩存token string strKey = string.Format("{0}{1}", WebConfig.signKey, signKey); Token cacheData = HttpRuntime.Cache.Get(strKey) as Token; if (cacheData == null) { cacheData = new Token(); cacheData.signId = signKey; cacheData.timespan = DateTime.Now.AddDays(1); cacheData.signToken = Guid.NewGuid().ToString("N"); //插入緩存,緩存時間為1天 HttpRuntime.Cache.Insert(strKey, cacheData, null, cacheData.timespan, TimeSpan.Zero); } //返回token信息 return Json<ResultMsg>(new ResultMsg((int)ExceptionStatus.OK, EnumExtension.GetEnumText(ExceptionStatus.OK), cacheData)); }
2.客戶端調用方法,GET或POST
(1) GET:需要在請求頭中添加:timespan(時間戳),nonce(隨機數),signKey(key),signature(簽名參數)
public static T Get<T>(string url, string paras, string signId,bool isSign=true) { HttpWebRequest webrequest = null; HttpWebResponse webresponse = null; string strResult = string.Empty; try { webrequest = (HttpWebRequest)WebRequest.Create(url + "?" + paras); webrequest.Method = "GET"; webrequest.ContentType = "application/json"; webrequest.Timeout = 90000; //加入頭信息 string timespan = GetTimespan(); string ran = GetRandom(10); webrequest.Headers.Add("signKey", signId); DbLogger.LogWriteMessage("signKey:" + signId); webrequest.Headers.Add("timespan", timespan); DbLogger.LogWriteMessage("timespan:" + timespan); webrequest.Headers.Add("nonce", ran); DbLogger.LogWriteMessage("nonce:" + ran); if (isSign) { string strSign = GetSignature(signId, timespan, ran, paras); webrequest.Headers.Add("signature", strSign); DbLogger.LogWriteMessage("signature:" + strSign); } webresponse = (HttpWebResponse)webrequest.GetResponse(); Stream stream = webresponse.GetResponseStream(); StreamReader sr = new StreamReader(stream, Encoding.UTF8); strResult = sr.ReadToEnd(); } catch (Exception ex) { return JsonConvert.DeserializeObject<T>(ex.Message); } finally { if (webresponse != null) webresponse.Close(); if (webrequest != null) webrequest.Abort(); } return JsonConvert.DeserializeObject<T>(strResult); }
(2)POST寫法這裏就不寫了,同理需要設置header請求頭參數:timespan(時間戳),nonce(隨機數),signKey(key),signature(簽名參數)
(3)根據請求參數計算本次請求的簽名,用timespan+nonc+signKey+token+data(請求參數字符串)得到signStr簽名字符串,然後再進行排序和MD5加密得到最終的signature簽名字符串,添加到請求頭中
public static string GetSignature(string signKey, string timespan, string nonce, string data) { string signToken = string.Empty; var result = GetToken<JObject>(); if (result != null) { if (result["code"].ToString() == "200") { var tokena = JsonConvert.DeserializeObject<JObject>(result["result"].ToString()); if (tokena != null) signToken = tokena["signToken"].ToString(); } } var hash = MD5.Create(); string str = signKey + timespan + nonce + signToken + data; byte[] bytes = Encoding.UTF8.GetBytes(string.Concat(str.OrderBy(c => c))); DbLogger.LogWriteMessage("str內容:" + string.Concat(str.OrderBy(c => c))); //使用MD5加密 var md5Val = hash.ComputeHash(bytes); //把二進制轉化為大寫的十六進制 StringBuilder strSign = new StringBuilder(); foreach (var val in md5Val) { strSign.Append(val.ToString("X2")); } return strSign.ToString(); }
(4)Webapi接收到相應參數,通過header獲取到timespan(時間戳),nonce(隨機數),signKey(key),signature(簽名參數),判斷參數是否為空、接口是否在有效時間內、判斷token是否有效、判斷和請求的signature(簽名)是否相同,如果通過,返回正常的結果。如果驗證不通過,返回相應的錯誤提示信息。
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext filterContext) { ResultMsg result = null; string signKey = string.Empty, timespan = string.Empty, nonce = string.Empty, signature = string.Empty; //判斷請求的消息中是否包括判斷參數 var request = filterContext.Request; if (request.Headers.Contains("signKey")) signKey = request.Headers.GetValues("signKey").FirstOrDefault(); if (request.Headers.Contains("timespan")) timespan = request.Headers.GetValues("timespan").FirstOrDefault(); if (request.Headers.Contains("nonce")) nonce = request.Headers.GetValues("nonce").FirstOrDefault(); if (request.Headers.Contains("signature")) signature = request.Headers.GetValues("signature").FirstOrDefault(); //如果方法是GetToken,則不需要驗證 if (filterContext.ActionDescriptor.ActionName.ToLower() == "gettoken") { if (string.IsNullOrEmpty(signKey) || string.IsNullOrEmpty(timespan) || string.IsNullOrEmpty(nonce)) { result = new ResultMsg((int)ExceptionStatus.ParameterError, EnumExtension.GetEnumText(ExceptionStatus.ParameterError), null); filterContext.Response = HttpResponseExtension.ToJson(result); base.OnActionExecuting(filterContext); return; } else { base.OnActionExecuting(filterContext); return; } } DbLogger.LogWriteMessage("測試參數"); string signtoken = string.Empty; //判斷是否包含以下參數 if (string.IsNullOrEmpty(signKey) || string.IsNullOrEmpty(timespan) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature)) { result = new ResultMsg((int)ExceptionStatus.ParameterError, EnumExtension.GetEnumText(ExceptionStatus.ParameterError), null); filterContext.Response = HttpResponseExtension.ToJson(result); base.OnActionExecuting(filterContext); return; } DbLogger.LogWriteMessage("測試是否在有效時間內"); //判斷是否在有效時間內 double ts1 = 0; double ts2 = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalMilliseconds; bool timespanValidate = double.TryParse(timespan, out ts1); double ts = ts2 - ts1; bool falg = ts > int.Parse(WebConfig.UrlExpireTime) * 1000; if (!timespanValidate || falg) { result = new ResultMsg((int)ExceptionStatus.URLExpireError, EnumExtension.GetEnumText(ExceptionStatus.URLExpireError), null); filterContext.Response = HttpResponseExtension.ToJson(result); base.OnActionExecuting(filterContext); return; } DbLogger.LogWriteMessage("測試token是否有效"); //判斷token是否有效 Token token = HttpRuntime.Cache.Get(string.Format("{0}{1}", WebConfig.signKey, signKey)) as Token; if (token == null) { result = new ResultMsg((int)ExceptionStatus.TokenInvalid, EnumExtension.GetEnumText(ExceptionStatus.TokenInvalid), null); filterContext.Response = HttpResponseExtension.ToJson(result); base.OnActionExecuting(filterContext); return; } else signtoken = token.signToken; DbLogger.LogWriteMessage("判斷http調用方式"); string data = string.Empty; //判斷http調用方式 string method = request.Method.Method.ToUpper(); 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": NameValueCollection form = HttpContext.Current.Request.QueryString; //第一步:取出所有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: result = new ResultMsg((int)ExceptionStatus.HttpMehtodError, EnumExtension.GetEnumText(ExceptionStatus.HttpMehtodError), null); filterContext.Response = HttpResponseExtension.ToJson(result); base.OnActionExecuting(filterContext); break; } DbLogger.LogWriteMessage("驗證簽名信息是否符合"); //驗證簽名信息是否符合 bool valida = ValidateSign.Validate(signKey, timespan, nonce, signtoken, data, signature); if (!valida) { result = new ResultMsg((int)ExceptionStatus.HttpRequestError, EnumExtension.GetEnumText(ExceptionStatus.HttpRequestError), null); filterContext.Response = HttpResponseExtension.ToJson(result); base.OnActionExecuting(filterContext); return; } else base.OnActionExecuting(filterContext); } }
下面我們進行測試:
GET請求:
返回結果:
但我們在瀏覽器中直接顯示或信息被串改時,不合法的請求就會被識別為請求參數已被修改
判斷簽名是否成功,第一次請求簽名參數signature和服務器端計算result完全相同, 然後當把請求參數修改之後服務器端計算的result和請求簽名參數signature不同,所以請求不合法,是非法請求,同理如果其他任何參數被修改最後計算的結果都會和簽名參數不同,請求同樣識別為不合法請求
總結:
通過上面的案例,我們可以看出,安全的關鍵在於參與簽名的token,整個過程中token是不參與通信的,所以只要保證token不泄露,請求就不會被偽造。
然後我們通過timestamp時間戳用來驗證請求是否過期,這樣就算被人拿走完整的請求鏈接也是無效的。
源碼下載地址:https://pan.baidu.com/s/1hrBOnRY
https://www.cnblogs.com/suzhiyong1988/p/7792457.html
記錄下Webapi簽名機制