程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WebApi基於Token和簽名的驗證,webapitoken簽名

WebApi基於Token和簽名的驗證,webapitoken簽名

編輯:關於.NET

WebApi基於Token和簽名的驗證,webapitoken簽名


最近一段時間在學習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

如果你覺得有幫助,請給原文大牛一個推薦,謝謝。

 

 

 

 

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved