最近公司有幾個項目須要開發手機客戶端,服務器端選用WebApi,那麼如何保證手機客戶端在請求服務器端時數據不被篡改,如何保證一個http請求的失效機制,下面總結一下咱們在項目中針對這兩個問題的解決方案。json
基本思路以下:api
用戶在成功登錄app客戶端以後,手機客戶端向服務器端發出的全部的http請求在請求頭(HttpHeader)上都會帶上下面三個參數:一、Uid(用戶ID),二、Ts(時間戳),三、Sign(簽名)。其中Ts是當前時間減去1970-1-1獲得的10位的時間時間戳數字,Sign是接口中全部http請求參數與Uid、Ts通過MD5加密後獲得的一個字符串。服務器
具體實現以下(客戶端的實現,手機客戶端生成下面兩個參數的思路是同樣的):app
一、Ts時間戳ide
Ts參數能夠保證請求的時效性,在手機客戶端生成的Ts,在服務器端驗證一下,保證請求是在咱們規定的時間段內,具體代碼以下:post
(1)、生成Ts(C#)代碼以下,Andriod和IOS能夠同理生成測試
/// <summary> /// 獲取十位的時間戳 /// </summary> /// <param name="dt"></param> /// <returns></returns> public string GenerateTimeStamp(DateTime dt) { // Default implementation of UNIX time of the current UTC time TimeSpan ts = dt.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(ts.TotalSeconds).ToString(); }
(2)、服務器端端驗證Ts代碼以下,咱們規定從手機客戶端發到服務器端的請求有效期爲5分鐘,時間戳參數是跟在Http請求頭中ui
//獲取請求頭信息 var requestHeader = HttpContext.Current.Request.Headers; //10位時間戳 var Ts = requestHeader.Get("Ts"); //驗證Ts是否合法(請求時間有效時間爲:加減5分鐘) var ts = Ts;//10位時間戳 if (ts.Length != 10) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "請求已過時", "application/json"); ; throw new HttpResponseException(resp); } var tsDate = ComHelper.ConvertIntDateTime(ts.ToString()); if (tsDate > DateTime.Now.AddMinutes(5) || tsDate < DateTime.Now.AddMinutes(-5)) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "請求已過時", "application/json"); ; throw new HttpResponseException(resp); }
(3)、ComHelper公共類代碼以下加密
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text; using System.Web; using System.Web.Security; namespace OpenAPITest.App_Start { public class ComHelper { /// <summary> /// 獲取post/get集合 /// </summary> /// <param name="ignoreCase">true 不區分大小寫,統一返回小寫 false 區分大小寫</param> /// <returns></returns> public static SortedDictionary<string, string> GetRequestSortDic(bool ignoreCase) { int i = 0; SortedDictionary<string, string> sArray = new SortedDictionary<string, string>(); NameValueCollection coll; //Load Form variables into NameValueCollection variable. coll = HttpContext.Current.Request.Form; //coll = HttpContext.Current.Request.Params; // Get names of all forms into a string array. String[] requestItem = coll.AllKeys; for (i = 0; i < requestItem.Length; i++) { if (ignoreCase) sArray.Add(requestItem[i].ToLower(), GetString(requestItem[i])); else sArray.Add(requestItem[i], GetString(requestItem[i])); } coll = HttpContext.Current.Request.QueryString; requestItem = coll.AllKeys; for (i = 0; i < requestItem.Length; i++) { if (ignoreCase) sArray.Add(requestItem[i].ToLower(), GetString(requestItem[i])); else sArray.Add(requestItem[i], GetString(requestItem[i])); } return sArray; } /// <summary> /// 從當前環境中獲取 /// </summary> /// <param name="name"></param> /// <param name="defValue"></param> /// <returns></returns> public static string GetString(string name) { string res = ""; var v = HttpContext.Current.Request[name]; if (v != null) { res = v.ToString(); } return res; } /// <summary> /// 時間戳轉爲C#格式時間 /// </summary> /// <param name="timeStamp"></param> /// <returns></returns> public static DateTime ConvertIntDateTime(string timeStamp) { DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); long lTime = long.Parse(timeStamp + "0000000"); TimeSpan toNow = new TimeSpan(lTime); return dtStart.Add(toNow); } /// <summary> /// MD5加密 /// </summary> /// <param name="str">原串</param> /// <param name="code">加密位</param> /// <returns></returns> public static string ToMD5(string str) { return FormsAuthentication.HashPasswordForStoringInConfigFile(str, "MD5").ToLower(); } /// <summary> /// 獲取Sign /// </summary> /// <param name="inputPara"></param> /// <param name="privateKey"></param> /// <returns></returns> public static string GetResponseMysign(SortedDictionary<string, string> inputPara, string privateKey) { string fullstring = GetPostStrings(inputPara, "_sign") + privateKey; return ToMD5(fullstring); } private static string GetPostStrings(SortedDictionary<string, string> inputPara, string excepted) { Dictionary<string, string> sPara = new Dictionary<string, string>(); //過濾空值、sign與sign_type參數 foreach (KeyValuePair<string, string> temp in inputPara) { if (temp.Key.ToLower() != excepted && temp.Value != "" && temp.Value != null) { sPara.Add(temp.Key.ToLower(), temp.Value); } } //得到簽名結果 StringBuilder prestr = new StringBuilder(); foreach (KeyValuePair<string, string> temp in sPara) { prestr.Append(temp.Key + "=" + temp.Value + "&"); } //去掉最後一個&字符 int nLen = prestr.Length; if (nLen > 1) prestr.Remove(nLen - 1, 1); return prestr.ToString(); } /// <summary> /// 獲取十位的時間戳 /// </summary> /// <param name="dt"></param> /// <returns></returns> public static string GenerateTimeStamp(DateTime dt) { // Default implementation of UNIX time of the current UTC time TimeSpan ts = dt.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(ts.TotalSeconds).ToString(); } } }
二、Sign簽名url
(1)、sign的生成規則:服務器端接口中的全部參數+Uid+Ts,去除掉參數中值爲空的參數後, 按照參數key值排序,用&連接,並所有轉化爲小寫,而後用MD5加密,經過HttpHeader發送到服務器端接口。
生成Sign大代碼以下(C#),Android和IOS能夠同理生成
假如手機客戶端請求的一個API接口爲:http://weapi.com/order/getlist?StatusID=1&CarID=2&CityID=3&name=&key=222 sign=md5(carid=2&cityid=3&key=222&statusid=1&ts=0123456789&uid=110)
(2)、驗證客戶端的Sign,防止參數被修改
//請求籤名,客戶端生成的簽名 var Sign = requestHeader.Get("Sign"); //排序字典,按照key排序 SortedDictionary<string, string> postValue = null; //獲取請求中全部的參數 postValue = ComHelper.GetRequestSortDic(true); postValue.Add("Uid", Uid);//API帳戶名稱 postValue.Add("Ts", Ts);//10位時間戳 string APIPrivateKey = "2D7E7C96-DAC5-4526-96C3-C60CDEC4B120";//客戶端和手機端保持一致 //服務器端生成的Sign string mysign = ComHelper.GetResponseMysign(postValue, APIPrivateKey); if (!Sign.Equals(mysign, StringComparison.InvariantCultureIgnoreCase)) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "簽名錯誤", "application/json"); ; throw new HttpResponseException(resp); }
三、模擬測試
(1)C#模擬Http請求,代碼以下
//請求的API地址 string url = "http://localhost:51942/api/Values/Get?StatusID=1&CarID=2&CityID=3&name=&key=1233"; //生成Ts string Ts = ComHelper.GenerateTimeStamp(DateTime.Now); //SortedDictionary會自動按照key值排序 SortedDictionary<string, string> sortDic = new SortedDictionary<string, string>(); sortDic.Add("StatusID", "1"); sortDic.Add("CarID", "2"); sortDic.Add("CityID", "3"); sortDic.Add("name", ""); sortDic.Add("key", "1233"); sortDic.Add("Uid","110"); sortDic.Add("Ts", Ts); string APIPrivateKey = "2D7E7C96-DAC5-4526-96C3-C60CDEC4B120";//客戶端和手機端保持一致,md5加密多使用了一個參數 //獲取Sign簽名 string Sign = ComHelper.GetResponseMysign(sortDic, APIPrivateKey); //發送請求 System.Net.WebRequest wReq = System.Net.WebRequest.Create(url); wReq.Headers.Add("Uid", "110"); wReq.Headers.Add("Ts", Ts); wReq.Headers.Add("Sign", Sign); System.Net.WebResponse wResp = wReq.GetResponse(); System.IO.Stream respStream = wResp.GetResponseStream(); using (System.IO.StreamReader reader = new System.IO.StreamReader(respStream, Encoding.UTF8)) { string res= reader.ReadToEnd(); }
(2)服務器端驗證參數,參數驗證寫在BaseApiController.cs文件中,只要繼承該類的均可以驗證客戶端傳過來的參數
public class ValuesController : BaseApiController { // GET api/values public IEnumerable<string> Get(string StatusID,string CarID,string CityID,string name, string key) { return new string[] { "value1", "value2" }; }
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Http.Controllers; using System.Net.Http; using System.Net; namespace OpenAPITest.App_Start { public class BaseApiController : ApiController { /// <summary> /// 初始化方法 /// </summary> /// <param name="controllerContext"></param> protected override void Initialize(HttpControllerContext controllerContext) { base.Initialize(controllerContext); //獲取請求頭信息 var requestHeader = HttpContext.Current.Request.Headers; //登陸用戶的ID var Uid = requestHeader.Get("Uid"); //請求籤名 var Sign = requestHeader.Get("Sign"); //10位時間戳 var Ts = requestHeader.Get("Ts"); //驗證請求頭信息是否合法 string errorMsg = CheckRequestHeader(Sign, Ts, Uid); //拋出錯誤信息 if (!string.IsNullOrWhiteSpace(errorMsg)) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "認證失敗", "application/json"); ; throw new HttpResponseException(resp); } //驗證Ts是否合法(請求時間有效時間爲:加減5分鐘) var ts = Ts;//10位時間戳 if (ts.Length != 10) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "請求已過時", "application/json"); ; throw new HttpResponseException(resp); } var tsDate = ComHelper.ConvertIntDateTime(ts.ToString()); if (tsDate > DateTime.Now.AddMinutes(5) || tsDate < DateTime.Now.AddMinutes(-5)) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "請求已過時", "application/json"); ; throw new HttpResponseException(resp); } //排序字典,按照key排序 SortedDictionary<string, string> postValue = null; //獲取請求中全部的參數 postValue = ComHelper.GetRequestSortDic(true); postValue.Add("Uid", Uid);//API帳戶名稱 postValue.Add("Ts", Ts);//10位時間戳 string APIPrivateKey = "2D7E7C96-DAC5-4526-96C3-C60CDEC4B120";//客戶端和手機端保持一致 //服務器端生成的Sign string mysign = ComHelper.GetResponseMysign(postValue, APIPrivateKey); if (!Sign.Equals(mysign, StringComparison.InvariantCultureIgnoreCase)) { var resp = Request.CreateResponse<string>(HttpStatusCode.OK, "簽名錯誤", "application/json"); ; throw new HttpResponseException(resp); } //驗證經過 } /// <summary> /// 驗證請求頭 /// </summary> /// <param name="Sign"></param> /// <param name="Ts"></param> /// <param name="Pid"></param> /// <returns></returns> private string CheckRequestHeader(string Sign, string Ts, string Uid) { string ErrorMsg = ""; //請求籤名 if (string.IsNullOrWhiteSpace(Sign)) { ErrorMsg = "認證失敗"; } //10位時間戳 else if (string.IsNullOrWhiteSpace(Ts)) { ErrorMsg = "認證失敗"; } //API帳戶名稱 else if (string.IsNullOrWhiteSpace(Uid)) { ErrorMsg = "認證失敗"; } return ErrorMsg; } } }