最近一段時間在學習WebApi,涉及到驗證部分的一些知識覺得自己並不是太懂,所以來博客園看了幾篇博文,發現一篇講的特別好的,讀了幾遍茅塞頓開(都閃開,我要裝逼了),剛開始讀有些地方不理解,所以想了很久,因此對原文中省略的部分這裡做一點個人的理解和補充,非常基礎,知道的園友就不需要了,只是幫助初次學習的園友理解。原文傳送門:
http://www.cnblogs.com/MR-YY/p/5972380.html#!comments
本篇博文中的所有代碼均來自上述鏈接,如果你覺得有幫助,請點擊鏈接給原文大牛一個推薦,開搞!!
1.基於Token令牌 + 簽名的驗證思路梳理
客戶端首先向服務端請求Token令牌,客戶獲取Token後計算對應的簽名。簽名由時間戳、隨機數、Token令牌、參數拼接字符串四部分組成,客戶端發送請求的時候需要帶上對應的身份ID、時間戳、隨機數和計算出的簽名。
服務端過濾器攔截請求,驗證請求參數的合法性、是否過期,Token令牌是否合法、是否過期,全部通過後重新計算簽名,與傳遞的簽名參數對比,一致則執行對應的Api請求,否則返回錯誤消息。如果服務端計算的簽名與傳遞的參數簽名不一樣,請求不合法(可能被篡改),為什麼這麼說呢,因為客戶端與服務端擁有相同的簽名計算方式,如果請求被修改,那麼服務端計算的簽名肯定與客戶端計算的簽名肯定不一致。
1.1 客戶端請求Token令牌流程
客戶端請求Token的憑證是對應的身份ID,當然可以是其他的,這裡假設用的是身份ID。
(1)首先客戶端向服務端發送獲取Token令牌的請求,這個Token令牌是一個GUID碼,生成後服務端會將其存在緩存中,當再次請求的時候會先從緩存中查找。請求Token令牌的代碼:
public static ProductResultMsg.TokenResultMsg GetSignToken(int staffId) { string tokenApi = AppSettingsConfig.GetTokenApi; Dictionary<string, string> parames = new Dictionary<string, string>(); parames.Add("staffid", staffId.ToString()); Tuple<string, string> parameters = GetQueryString(parames); ProductResultMsg.TokenResultMsg token = WebApiHelper.Get<ProductResultMsg.TokenResultMsg> (tokenApi, parameters.Item1, staffId.ToString(), staffId, false); return token; }
代碼解釋:
1.tokenApi是配置在webConfig中的接口Url
2.Parames字典對象用來封裝參數的,因為請求Token時可能不止一個參數。
3.Parameter:是元組類型,元組可以承載任何的數據類型,這裡用來接收GetQueryString方法返回的拼接字符串
4.token:客戶端用來承載接口返回的Token令牌的類的實例,TokenResultMSg結構如下:
public class TokenResultMsg : HttpResponseMsg { public Tokens Result { get { if (StatusCode == (int)StatusCodeEnum.Success) { return JsonConvert.DeserializeObject<Tokens>(Data.ToString()); } return null; } } }
可以看到這個類實際是封裝了一個Token類實例,因為Api接口返回的是Json數據類型,所以這裡進行了反序列化。Token類結構在最上面,裡面包含身份ID、 Token令牌、 過期時間三個屬性,最後返回包含Token令牌的Token類給主程序。Token類結構如下:
public class Tokens { /// <summary> /// 用戶名 /// </summary> public int StaffId { get; set; } /// <summary> /// 用戶名對應簽名Token /// </summary> public Guid SignToken { get; set; } /// <summary> /// Token過期時間 /// </summary> public DateTime ExpireTime { get; set; } }
(2)上面調用了GetQueryString方法,這個方法是用來整理參數以及參數的拼接字符串,參數用來隨URL傳遞,參數的拼接字符串用來生成對應的Sign簽名,方法如下:
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)); }
(3)此時Url以及參數已經准備完成,之後調用了 Get 方法,發送獲取Token的請求到WepApi接口,這裡描述一下思路,請求的Get方法你可以做成單獨的,也可以做成公共的,公共的是什麼意思呢,就是這個Get方法不止可以用來請求Token令牌,請求的參數視安全程度決定,這裡為了演示只傳遞了一個身份ID,在實際的操作過程中可以傳遞更多的驗證數據,Get方法代碼如下:(對於後台請求Api接口的方式這裡就不做詳解了)
public static T Get<T>(string webApi, string query, string queryStr, int staffId, bool sign = true) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(webApi + "/" + queryStr); string timeStamp = GetTimeStamp(); string nonce = GetRandom(); //加入頭信息 request.Headers.Add("staffid", staffId.ToString());//當前請求用戶的Staffid request.Headers.Add("timestamp", timeStamp);//發起請求的時間戳(單位:毫秒) request.Headers.Add("nonce", nonce);//發起請求的隨機數 if (sign) request.Headers.Add("signature", GetSignature(timeStamp, nonce, staffId, query));//當前請求內容的數字簽名 request.Method = "GET"; request.ContentType = "application/json"; request.Timeout = 90000; request.Headers.Set("Pragma", "no-cache"); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream streamReceive = response.GetResponseStream(); StreamReader streamReader = new StreamReader(streamReceive, Encoding.UTF8); string strResult = streamReader.ReadToEnd(); streamReader.Close(); streamReceive.Close(); request.Abort(); response.Close(); return JsonConvert.DeserializeObject<T>(strResult); }
方法中有兩個地方需要在這裡說一下,因為博主當時第一次看的時候沒注意,那就是時間戳和隨機數的生成,代碼如下:
/// 獲得時間戳 /// </summary> /// <returns></returns> private static string GetTimeStamp() { TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(ts.TotalMilliseconds).ToString(); }
/// <summary> /// 獲取隨機數 /// </summary> /// <returns></returns> private static string GetRandom() { Random rd = new Random(DateTime.Now.Millisecond); int i = rd.Next(0, int.MaxValue); return i.ToString(); }
(4)當代碼執行到HttpWebResponse response = (HttpWebResponse)request.GetResponse();時客戶端發送請求發到Api接口,此時我們來看Api接口收到請求的處理程序:
public class ServiceController : ApiController { [HttpGet] public HttpResponseMessage GetToken(string id) { string staffId = id; 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 = ""; string returnErrJson = Newtonsoft.Json.JsonConvert.SerializeObject(resultMsg); return new HttpResponseMessage { Content = new StringContent(returnErrJson, System.Text.Encoding.UTF8) }; } //插入緩存 Tokens token = (Tokens)HttpRuntime.Cache.Get(id.ToString()); if (HttpRuntime.Cache.Get(id.ToString()) == null) { token = new Tokens(); 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; string returnJson = Newtonsoft.Json.JsonConvert.SerializeObject(resultMsg); return new HttpResponseMessage { Content = new StringContent(returnJson, System.Text.Encoding.UTF8) }; }
代碼分析:
服務端收到請求後,接受傳過來的身份ID,首先判斷ID是否為空以及是否合法以及其它一些驗證,如果有什麼地方不合法,就返回錯誤信息到客戶端。
如果身份ID符合所有驗證,則根據ID去緩存中查詢對應的Token令牌,如果緩存中沒有,則新生成對應身份ID的Token令牌,令牌是一段GUID碼,生成之後存入緩存中。
最後,返回一個包含Token的 ResultMsg實體,記得轉換為Json數據類型,ResultMsg類結構如下,Data屬性是object類型,用來承載Token類對象:
public class ResultMsg { /// <summary> /// 狀態碼 /// </summary> public int StatusCode { get; set; } /// <summary> /// 操作信息 /// </summary> public string Info { get; set; } /// <summary> /// 返回數據 /// </summary> public object Data { get; set; } }
到此,Token令牌就返回到了客戶端,這個過程的執行狀態可以根據實際情況決定,比如是請求一次之後將Token令牌存在客戶端,或者是每次發送請求到客戶端都需要請求一次Token令牌。
1.2 發送數據請求完整描述
在客戶端得到Token令牌之後每次請求都需要帶上它,當然,只有Token令牌還是不夠的,還需要編碼的簽名。簽名由4個部分組成:時間戳、隨機數、Token令牌、數據參數,編碼的方式如下,首先將這四部分進行拼接字符串,然後將字符串中字符按照升序排序,之後轉換為二進制數據流,然後進行MD5哈希加密,MD5是哈希加密的一種,接著循環遍歷加密後的二進制字節流,這個時候字節流的長度時128位的,為了使用方便和節約網絡傳輸流量我們需要把它轉化為16進制的字符串,最後將所有的字符串轉換為大寫。至此,加密簽名完成。生成Signature簽名的代碼如下:
public static string GetSignature(string timeStamp, string nonce, int staffId, string data) { Tokens 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 v in md5Val) { result.Append(v.ToString("X2")); } return result.ToString().ToUpper(); }
在發送數據請求的時候,需要傳遞四個參數,時間戳(用來判斷請求是否過期)、隨機數(用來強化請求的安全性)、身份ID(服務端用來查詢對應的Token令牌)、Signature簽名。
那麼服務端應該怎麼驗證用戶的請求是否合法呢?服務端通過一個全局的過濾器(Filter)來攔截所有的客戶端請求(不包含請求Token令牌),過濾器攔截到請求後首先判斷請求方式,根據不同的請求方式獲取請求中所有的參數(比如Get是QueryString,Post是InputStream),然後通過參數名稱(key)得到參數的值(value),然後對參數進行相應的驗證(是否為空或null),通過TimeSpan時間戳判斷請求是否過期,如果過期則返回對應的錯誤信息;通過身份ID查詢緩存中的Token令牌,與參數中令牌對比是否正確,如果驗證都通過則對最後的簽名做驗證。
服務端驗證簽名的方式是這樣的,使用與客戶端計算簽名同樣的算法,重新計算簽名字符串,然後與請求中的前民字符串做對比,這裡有個問題是,我在注冊為全局過濾器的時候,系統提示需要實現System.Web.Mvc中四個接口中的一個,但是原文中實現的是System.Web.Http命名空間下的標簽類,博主有點迷糊了,如果有懂得園友大牛可以指點一下,服務端過濾器代碼如下:
public class ApiSecurityFilter:ActionFilterAttribute,IActionFilter { public override void OnActionExecuting(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 = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") }; return; } else { base.OnActionExecuting(actionContext); } } //判斷請求頭是否包含以下參數 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 = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") }; 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 = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") }; base.OnActionExecuting(actionContext); return; } //判斷Token是否有效 Tokens token = (Tokens)HttpRuntime.Cache.Get(id.ToString()); string signtoken = string.Empty; if (HttpRuntime.Cache.Get(id.ToString()) == null) { //說明身份ID對應的token令牌不存在 resultMsg = new ResultMsg(); resultMsg.StatusCode = (int)StatusCodeEnum.TokenInvalid; resultMsg.Info = StatusCodeEnum.TokenInvalid.GetEnumText(); resultMsg.Data = ""; actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") }; base.OnActionExecuting(actionContext); return; } else { //緩存中存在ID對應的 Token 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 = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") }; 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 = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") }; base.OnActionExecuting(actionContext); return; } else { base.OnActionExecuting(actionContext); } } }
如果服務端與客戶端簽名也一致,所有驗證通過,根據請求執行對應的Api方法,返回結果。這裡就不寫代碼了,再次奉上原文大牛連接:
http://www.cnblogs.com/MR-YY/p/5972380.html#!comments
如果你覺得有幫助,請給原文大牛一個推薦,謝謝。