WebApi安全性 使用TOKEN+簽名驗證

首先問你們一個問題,你在寫開放的API接口時是如何保證數據的安全性的?先來看看有哪些安全性問題在開放的api接口中,咱們經過http Post或者Get方式請求服務器的時候,會面臨着許多的安全性問題,例如:git

  1. 請求來源(身份)是否合法?
  2. 請求參數被篡改?
  3. 請求的惟一性(不可複製),防止請求被惡意攻擊

爲了保證數據在通訊時的安全性,咱們能夠採用TOKEN+參數簽名的方式來進行相關驗證。github

 

 

好比說咱們客戶端須要查詢產品信息這個操做來進行分析,客戶端點擊查詢按鈕==》調用服務器端api進行查詢==》服務器端返回查詢結果web

1、不進行驗證的方式算法

api查詢接口:json

客戶端調用:http://api.XXX.com/getproduct?id=value1api

如上,這種方式簡單粗暴,在瀏覽器直接輸入"http://api.XXX.com/getproduct?id=value1",便可獲取產品列表信息了,可是這樣的方式會存在很嚴重的安全性問題,沒有進行任何的驗證,你們均可以經過這個方法獲取到產品列表,致使產品信息泄露。
那麼,如何驗證調用者身份呢?如何防止參數被篡改呢?如何保證請求的惟一性? 如何保證請求的惟一性,防止請求被惡意攻擊呢?瀏覽器

 

2、使用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/tea-yy/TokenSign

相關文章
相關標籤/搜索