記錄下Webapi簽名機制

来源:http://www.cnblogs.com/suzhiyong1988/archive/2017/11/06/7792457.html
-Advertisement-
Play Games

首先,寫這篇文章的原因是因為最近某一個項目中的介面被人為調用了,導致了資料庫數據被串改。雖然是內部人無意點的,但還是引起了我的擔憂,所有整理了下關於Webapi的相關簽名機制。 一、我們在開發介面時,有時候嫌麻煩就懶進行相關的驗證或只進行一些簡單的驗證,這樣客戶端就可以直接調用:如 調用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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、負載均衡集群介紹 1、集群 ① 集群(cluster)技術是一種較新的技術,通過集群技術,可以在付出較低成本的情況下獲得在性能、可靠性、靈活性方面的相對較高的收益,其任務調度則是集群系統中的核心技術。 ② 集群是一組相互獨立的、通過高速網路互聯的電腦,它們構成了一個組,並以單一系統的模式加以管 ...
  • 項目簡介和code見《同步非同步和阻塞2-測試小項目》 1. 實現 由於IO是阻塞的,所以要實現輪詢IO的結果,需要將IO放入線程中處理,IO的處理結果作為給線程的exit code返回。這裡用“CBaseThread”簡單的將線程處理函數封裝到類中 在OnStart()中,先依次啟動2個線程處理IO ...
  • 因為windows是不能引導linux的,而每次win10升級或恢復都會將linux的啟動引導覆蓋掉,導致無法進入linux, 所以一直就禁止了win10更新.這幾天win10出了點小毛病,所以就狠下心來恢復了系統, 好吧~_~ 這下嗝屁了,ubuntu進不去了.裡面保存了不少資料,實在是不想重裝, ...
  • Linux/Unix 命令格式: 命令名 [選項] [參數] 註:[]中的內容代表內容可以省略 例:$ ls $ ls -l #-l 是選項 開始符號: 文件名 或 文件夾名 .當前文件夾 ..上一級文件夾 ~用戶主目錄(家目錄) 查看類: pwd命令: 使用: 用於顯示當前操作的位置的路徑(當前工 ...
  • 新的剛來到,舊的就忘掉。學習 AspNet Core 2.0,沒有好的例子,是很痛苦的。《Pro ASP.NET Core MVC 2》中的 SportsStore 值得一看,不妨下載研究一下: 準備 1. 使用 Ubuntu 系統 2. 安裝 NetCore2.0 3. 安裝 VSCode 4. ...
  • 最近開發一個新項目,使用了asp.net core 2.0,採用webapi開發後臺,postgresql為資料庫。最先來的問題就是上傳文件的問題。 POST文件的一些坑 使用預設模板創建webapi的controller後,post請求,預設有 請求使用了 標記,用來指示用請求體里獲得數據。 對於 ...
  • 本次是結合近乎免費源碼版產品代碼進行的介紹,關於近乎產品下載可以訪問:www.jinhusns.com 下載免費源碼版本瞭解。 ...
  • 一、Model層 二、控制器層 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Mvc; 6 using Mvc_Demo. ...
一周排行
    -Advertisement-
    Play Games
  • 一:背景 1. 講故事 前些天有位朋友找到我,說他們的程式會偶發性的卡死一段時間,然後又好了,讓我幫忙看下怎麼回事?窗體類的程式解決起來相對來說比較簡單,讓朋友用procdump自動抓一個卡死時的dump,拿到dump之後,上 windbg 說話。 二:WinDbg 分析 1. 主線程在做什麼 要想 ...
  • 功能說明 使用ListView時,希望可以在單元格顯示圖片或其他控制項,發現原生的ListView不支持,於是通過拓展,實現ListView可以顯示任意控制項的功能,效果如下: 實現方法 本來想著在單元格裡面實現控制項的自繪的,但是沒找到辦法,最後是通過在單元格的錶面顯示對應控制項的,浮於錶面達到目的。 實 ...
  • 由於.NET Framework 4.0 是比較古老的版本,只有New Relic 7.0以下的版本才會支持.NET Framework 4.0的引用程式。 Technical support for .NET Framework 4.0 or lower 你可以參考這個官方Install New ...
  • 前言 隨著 DEV24.1.3 的發佈,XAF Blazor 中的屬性編輯器(PropertyEditor)也進行了很大的改動,在使用體驗上也更接近 WinForm 了,由於進行了大量的封裝,理解上沒有 WinForm 直觀,所以本文通過對屬性編輯器的原理進行解析,並對比新舊版本中的變化,使大家能夠 ...
  • OPC基金會提供了OPC UA .NET標準庫以及示常式序,但官方文檔過於簡單,光看官方文檔和示常式序很難弄懂OPC UA .NET標準庫怎麼用,花了不少時間摸索才略微弄懂如何使用,以下記錄如何從一個控制台程式開發一個OPC UA伺服器。 安裝Nuget包 安裝OPCFoundation.NetSt ...
  • 今天在技術群里,石頭哥向大家提了個問題:"如何在一個以System身份運行的.NET程式(Windows Services)中,以其它活動的用戶身份啟動可互動式進程(桌面應用程式、控制台程式、等帶有UI和互動式體驗的程式)"? 我以前有過類似的需求,是在GitLab流水線中運行帶有UI的自動化測試程 ...
  • .Net 中提供了一系列的管理對象集合的類型,數組、可變列表、字典等。從類型安全上集合分為兩類,泛型集合 和 非泛型集合,傳統的非泛型集合存儲為Object,需要類型轉。而泛型集合提供了更好的性能、編譯時類型安全,推薦使用。 ...
  • 在以前我做程式的時候,一般在登錄視窗裡面顯示程式名稱,登錄視窗一般設置一張背景圖片,由於程式的名稱一般都是確定的,所以也不存在太大的問題,不過如果客戶定製不同的系統的時候,需要使用Photoshop修改下圖層的文字,再生成圖片,然後替換一下也可以了。不過本著減少客戶使用繁瑣性,也可以使用空白名稱的通... ...
  • 一:背景 1. 講故事 在dump分析的過程中經常會看到很多線程卡在Monitor.Wait方法上,曾經也有不少人問我為什麼用 !syncblk 看不到 Monitor.Wait 上的鎖信息,剛好昨天有時間我就來研究一下。 二:Monitor.Wait 底層怎麼玩的 1. 案例演示 為了方便講述,先 ...
  • 目錄前言學習參考過程總結: 前言 做個自由仔。 學習參考 ChatGpt; https://www.cnblogs.com/zhili/p/DesignPatternSummery.html(大佬的,看了好多次) 過程 原由: 一開始只是想查查鏈式調用原理,以為是要繼承什麼介面,實現什麼方法才可以實 ...