分享api介面驗證模塊

来源:http://www.cnblogs.com/4littleProgrammer/archive/2016/12/26/6220351.html
-Advertisement-
Play Games

一.前言 許可權驗證在開發中是經常遇到的,通常也是封裝好的模塊,如果我們是使用者,通常指需要一個標記特性或者配置一下就可以完成,但實際裡面還是有許多東西值得我們去探究。有時候我們也會用一些開源的許可權驗證框架,不過能自己實現一遍就更好,自己開發的東西成就感(逼格)會更高一些。進入主題,本篇主要是介紹介面 ...


一.前言

  許可權驗證在開發中是經常遇到的,通常也是封裝好的模塊,如果我們是使用者,通常指需要一個標記特性或者配置一下就可以完成,但實際裡面還是有許多東西值得我們去探究。有時候我們也會用一些開源的許可權驗證框架,不過能自己實現一遍就更好,自己開發的東西成就感(逼格)會更高一些。進入主題,本篇主要是介紹介面端的許可權驗證,這個部分每個項目都會用到,所以最好就是也把它插件化,放在Common中,新的項目就可以直接使用了。基於web的驗證之前也寫過這篇,有興趣的看一下ASP.NET MVC Form驗證

二.簡介

  對於我們系統來說,提供給外部訪問的方式有多種,例如通過網頁訪問,通過介面訪問等。對於不同的操作,訪問的許可權也不同,如:

      1. 可直接訪問。對於一些獲取數據操作不影響系統正常運行的和數據的,多餘的驗證是沒有必要的,這個時候可以直接訪問,例如獲取當天的天氣預報信息,獲取網站的統計信息等。

      2. 基於表單的web驗證。對於網站來說,有些網頁需要我們登錄才可以操作,http請求是無狀態,用戶每次操作都登錄一遍也是不可能的,這個時候就需要將用戶的登錄狀態記錄在某個地方。基於表單的驗證通常是把登錄信息記錄在Cookie中,Cookie每次會隨請求發送到服務端,以此來進行驗證。例如博客園,會把登錄信息記錄在一個名稱為.CNBlogsCookie的Cookie中(F12可去掉cookie觀察效果),這是一個經過加密的字元串,服務端會進行解密來獲取相關信息。當然雖然進行加密了,但請求在網路上傳輸,依據可能被竊取,應對這一點,通常是使用https,它會對請求進行非對稱加密,就算被竊取,也無法直接獲得我們的請求信息,大大提高了安全性。可以看到博客園也是基於https的。

  3. 基於簽名的api驗證。對於介面來說,訪問源可能有很多,網站、移動端和桌面程式都有可能,這個時候就不能通過cookie來實現了。基於簽名的驗證方式理論很簡單,它有幾個重要的參數:appkey, random,timestamp,secretkey。secretkey不隨請求傳輸,服務端會維護一個 appkey-secretkey 的集合。例如要查詢用戶餘額時,請求會是類似:/api/user/querybalance?userid=1&appkey=a86790776dbe45ca9032fc59bbc351cb&random=191&timestamp=14826791236569260&sign=09d72f207ba8ca9c0fd0e5f8523340f5 

參數解析:

  1.appkey用於給服務端找到對應的secretkey。有時候我們會分配多對appkey-secretkey,例如安卓分一對,ios分一對。

  2.random、timestamp是為了防止重放攻擊的(Repaly Attacks),這是為了避免請求被竊取後,攻擊者通過分析後破解後,再次發起惡意請求。參數timestamp時間戳是必須的,所謂時間戳是指從1970-1-1至當前的總秒數。我們規定一個時間,例如20分鐘,超過20分鐘就算過期,如果當前時間與這個時間戳的間隔超過20分鐘,就拒絕。random不是必須的,但有了它也可以更好防止重放攻擊,理論上來說,timestamp+random應該是唯一的,這個時候我們可以將其作為key緩存在redis,如果通過請求的timestamp+random能在規定時間獲取到,就拒絕。這裡還有個問題,客戶端與服務端時間不同步怎麼辦?這個可以要求客戶端校正時間,或者把過期時間調大,例如30分鐘才算過期,再或者可以使用網路時間。防止重放攻擊也是很常見的,例如你可以把手機時間調到較早前一個時間,再使用手機銀行,這個時候就會收到error了。

     3.sign簽名是通過一定規則生成,在這裡我用sign=md5(httpmethod+url+timestamp+參數字元串+secretkey)生成。服務端接收到請求後,先通過appkey找到secretkey,進行同樣拼接後進行hash,再與請求的sign進行比較,不一致則拒絕。這裡需要註意的是,雖然我們做了很多工作,但依然不能阻止請求被竊取;我把timestamp參與到sign的生成,因為timestamp在請求中是可見的,請求被竊取後它完全可以被修改並再次提交,如果我們把它參與到sign的生成,一旦修改,sign也就不一樣了,提高了安全性。參數字元串是通過請求參數拼接生成的字元串,目的也是類似的,防止參數被篡改。例如有三個參數a=1,b=3,c=2,那麼參數字元串=a1b3c2,也可以通過將參數按值進行排序再拼接生成參數字元串。

  使用例子,最近剛好在使用友盟的消息推送服務,可以看到它的簽名生成規則如下,與我們介紹是類似的。

三.編碼實現

   這裡還是通過Action Filter來實現的,具體可以看通過源碼瞭解ASP.NET MVC 幾種Filter的執行過程介紹。通過上面的簡介,這裡的代碼雖多,但很容易理解了。ApiAuthorizeAttribute 是標記在Action或者Controller上的,定義如下

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class ApiAuthorizeAttribute : ApiBaseAuthorizeAttribute
    {
        private static string[] _keys = new string[] { "appkey", "timestamp", "random", "sign" };

        public override void OnAuthorization(AuthorizationContext context)
        {
            //是否允許匿名訪問
            if (context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false))
            {
                return;
            }
            HttpRequestBase request = context.HttpContext.Request;
            string appkey = request[_keys[0]];
            string timestamp = request[_keys[1]];
            string random = request[_keys[2]];
            string sign = request[_keys[3]];
            ApiStanderConfig config = ApiStanderConfigProvider.Config;
            if(string.IsNullOrEmpty(appkey))
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissAppKey);
                return;
            }
            if (string.IsNullOrEmpty(timestamp))
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissTimeStamp);
                return;
            }
            if (string.IsNullOrEmpty(random))
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissRamdon);
                return;
            }
            if(string.IsNullOrEmpty(sign))
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissSign);
                return;
            }
            //驗證key
            string secretKey = string.Empty;
            if(!SecretKeyContainer.Container.TryGetValue(appkey, out secretKey))
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.KeyNotFound);
                return;
            }
            //驗證時間戳(時間戳是指1970-1-1到現在的總秒數)      
            long lt = 0;
            if (!long.TryParse(timestamp, out lt))
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.TimeStampTypeError);
                return;
            }
            long now = DateTime.Now.Subtract(new DateTime(1970, 1, 1)).Ticks;
            if (now - lt > new TimeSpan(0, config.Minutes, 0).Ticks)
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.PastRequet);
                return;
            }
            //驗證簽名
            //httpmethod + url + 參數字元串 + timestamp + secreptkey
            MD5Hasher md5 = new MD5Hasher();
            string parameterStr = GenerateParameterString(request);
            string url = request.Url.ToString();
            url = url.Substring(0, url.IndexOf('?'));
            string serverSign = md5.Hash(request.HttpMethod + url + parameterStr + timestamp + secretKey);
            if(sign != serverSign)
            {
                SetUnAuthorizedResult(context, ApiUnAuthorizeType.ErrorSign);
                return;
            }
        }

        private string GenerateParameterString(HttpRequestBase request)
        {
            string parameterStr = string.Empty;
            var collection = request.HttpMethod == "GET" ? request.QueryString : request.Form;
            foreach(var key in collection.AllKeys.Except(_keys))
            {
                parameterStr += key + collection[key] ?? string.Empty;
            }
            return parameterStr;
        }
    }

  下麵會對這段核心代碼進行解析。ApiStanderConfig包裝了一些配置信息,例如上面我們說到的過期時間是20分鐘,但我們希望可以在模塊外部進行自定義。所以通過一個ApiStanderConfig來包裝,通過ApiStanderConfigProvider來註冊和獲取。ApiStanderConfig和ApiStanderConfigProvider的定義如下

    public class ApiStanderConfig
    {
        public int Minutes { get; set; }
    }  
    public class ApiStanderConfigProvider
    {
        public static ApiStanderConfig Config { get; private set; }

        static ApiStanderConfigProvider()
        {
            Config = new ApiStanderConfig()
            {
                Minutes = 20
            };
        }

        public static void Register(ApiStanderConfig config)
        {
            Config = config;
        }
    }

  前面介紹到服務端會維護一個appkey-secretkey的集合,這裡通過一個SecretKeyContainer實現,它的Container就是一個字典集合,定義如下

    public class SecretKeyContainer
    {
        public static Dictionary<string, string> Container { get; private set; }

        static SecretKeyContainer()
        {
            Container = new Dictionary<string, string>();
        }

        public static void Register(string appkey, string secretKey)
        {
            Container.Add(appkey, secretKey);
        }

        public static void Register(Dictionary<string, string> set)
        {
            foreach(var key in set)
            {
                Container.Add(key.Key, key.Value);
            }
        }
    }

  可以看到,上面有很多的條件判斷,並且錯誤會有不同的描述。所以我定義了一個ApiUnAuthorizeType錯誤類型枚舉和DescriptionAttribute標記,如下:

    public enum ApiUnAuthorizeType
    {
        [Description("時間戳類型錯誤")]
        TimeStampTypeError = 1000,

        [Description("appkey缺失")]
        MissAppKey = 1001,

        [Description("時間戳缺失")]
        MissTimeStamp = 1002,

        [Description("隨機數缺失")]
        MissRamdon = 1003,

        [Description("簽名缺失")]
        MissSign = 1004,

        [Description("appkey不存在")]
        KeyNotFound = 1005,

        [Description("過期請求")]
        PastRequet = 1006,

        [Description("錯誤的簽名")]
        ErrorSign = 1007
    }
    public class DescriptionAttribute : Attribute
    {
        public string Description { get; set; }

        public DescriptionAttribute(string description)
        {
            Description = description;
        }
    }

  當驗證不通過時,會調用SetUnAuthorizedResult,並且請求不需再進行下去了。這個方法是在基類中實現的,如下

    public class ApiBaseAuthorizeAttribute : AuthorizeAttribute
    {
        protected virtual void SetUnAuthorizedResult(AuthorizationContext context, ApiUnAuthorizeType type)
        {
            UnAuthorizeHandlerProvider.ApiHandler(context, type);
            HandleUnauthorizedRequest(context);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.Result != null)
            {
                return;
            }
            base.HandleUnauthorizedRequest(filterContext);
        }
    }

  可以看到,它通過一個委托根據錯誤類型處理結果,UnAuthorizeHandlerProvider定義如下

    public class UnAuthorizeHandlerProvider
    {
        public static Action<AuthorizationContext, ApiUnAuthorizeType> ApiHandler { get; private set; }

        static UnAuthorizeHandlerProvider()
        {
            ApiHandler = ApiUnAuthorizeHandler.Handler;
        }

        public static void Register(Action<AuthorizationContext, ApiUnAuthorizeType> action)
        {
            ApiHandler = action;
        }
    }    

  它預設通過ApiUnAuthorizeHandler.Handler來處理結果,但也可以在模塊外部進行註冊。預設的處理為ApiUnAuthorizeHandler.Handler,如下

    public class ApiUnAuthorizeHandler
    {
        public readonly static Action<AuthorizationContext, ApiUnAuthorizeType> Handler = (context, type) =>
        {
            context.Result = new StanderJsonResult()
            {
                Result = FastStatnderResult.Fail(type.GetDescription(), (int)type)
            };
        };
    }

  它的操作就是返回一個json結果。type.GetDescription是一個擴展方法,目的就是獲取DescriptionAttribute的描述信息,如下

    public static class EnumExt
    {
        public static string GetDescription(this Enum e)
        {
            Type type = e.GetType();
            var attributes = type.GetField(e.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
            if(attributes.IsNullOrEmpty())
            {
                return null;
            }
            return attributes[0].Description;
        }
    }

  這裡還涉及到幾個json相關對象,但它們應該不影響閱讀。StanderResult, FastStanderResult, StanderJsonResult,有興趣也可以看一下,在實際項目中有很多地方都可以用到它們,可以標準和簡化許多操作。如下

    public class StanderResult
    {
        public bool IsSuccess { get; set; }

        public object Data { get; set; }

        public string Description { get; set; }

        public int Code { get; set; }
    }

    public static class FastStatnderResult
    {
        private static StanderResult _success = new StanderResult() { IsSuccess = true };
 
        public static StanderResult Success()
        {
            return _success;
        }

        public static StanderResult Success(object data, int code = 0)
        {
            return new StanderResult() { IsSuccess = true, Data = data, Code = code };
        }

        public static StanderResult Fail()
        {
            return new StanderResult() { IsSuccess = false };
        }

        public static StanderResult Fail(string description, int code = 0)
        {
            return new StanderResult() { IsSuccess = false, Description = description, Code = code };
        }
    }  
    public class StanderJsonResult : ActionResult
    {
        public StanderResult Result { get; set; }

        public string ContentType { get; set; }

        public Encoding Encoding { get; set; }

        public override void ExecuteResult(ControllerContext context)
        {
            HttpResponseBase response = context.HttpContext.Response;
            response.ContentType = string.IsNullOrEmpty(ContentType) ?
                "application/json" : ContentType;

            if (Encoding != null)
            {
                response.ContentEncoding = Encoding;
            }
            string json = JsonConvert.SerializeObject(Result);
            response.Write(json);
        }
    }

四.例子

  我們在程式初始化時註冊appkey-secretkey,如

            //註冊appkey-secretkey
            string[] appkey1 = ConfigurationReader.GetStringValue("appkey1").Split(',');
            SecretKeyContainer.Container.Add(appkey1[0], appkey1[1]);

  下麵的使用就很簡單了,標記需要驗證的介面。如

        [ApiAuthorize]
        public ActionResult QueryBalance(int userId)
        {
            return Json("查詢成功");
        }

  我們在網頁輸入鏈接測試:如

      1.輸入過期時間會提示{"IsSuccess":false,"Data":null,"Description":"過期請求","Code":1006}

      2.輸入錯誤簽名會提示{"IsSuccess":false,"Data":null,"Description":"錯誤的簽名","Code":1007}

  只有所有驗證都成功時才可以訪問。

  當然實際項目的驗證可能會更複雜一些,條件也會更多一些,不過都可以在此基礎上進行擴展。如上面所說,這種演算法可以保證請求是合法的,而且參數不被篡改,但還是無法保證請求不被竊取,要實現更高的安全性還是需要使用https。


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

-Advertisement-
Play Games
更多相關文章
  • 存儲過程語法、概念、介紹;如何處理並記錄複雜存儲過程中發生的錯誤 ...
  • 本文出處:http://www.cnblogs.com/wy123/p/6217772.html 字元串自身相加, 雖然賦值給了varchar(max)類型的變了,在某些特殊情況下仍然會被“截斷”,這到底是varchar(max)長度的問題還是操作的問題? 1,兩個不超過8000長度的字元串自身相加 ...
  • 隔離級別定義事務操作資源和更新數據的隔離程度,在SQL Server中,隔離級別隻會影響讀操作申請的共用鎖,而不會影響寫操作申請的互斥鎖。隔離級別控制事務在執行讀操作時: 在讀數據時是否使用共用鎖,申請何種類型的隔離級別; 事務持有讀鎖的時間 讀操作引用其他事務更新的數據行時,控制讀操作的行為: 被 ...
  • Linux環境下寫代碼雖然沒有IDE,但通過給vim配置幾個插件也足夠好用。一般常用的插件主要包括幾類,查找文件,查找符號的定義或者聲明(函數,變數等)以及自動補全功能。一般流程都是下載需要的工具,然後在vimrc文件中配置載入工具選項,一直這麼用也沒覺得啥。但最近發現通過vundle工具可以很方便 ...
  • TCP/IP之Nagle演算法與40ms延遲提到了Nagle 演算法。這樣雖然提高了網路吞吐量,但是實時性卻降低了,在一些交互性很強的應用程式來說是不允許的,使用TCP_NODELAY選項可以禁止Nagle 演算法。禁止Nagle後應用程式向內核遞交的每個數據包都會立即發送出去。但是禁止Nagle,網路傳 ...
  • Nagle演算法是針對網路上存在的微小分組可能會在廣域網上造成擁塞而設計的。該演算法要求一個TCP連接上最多只能有一個未被確認的未完成的小分組,在該分組確認到達之前不能發送其他的小分組。同時,TCP收集這些少量的分組,併在確認到來時以一個分組發出去。它的設計規則如下: (1)如果包長度達到最大報文長度( ...
  • 1.1在這之前,我們需要瞭解程式的編譯過程 a.預處理:檢查語法錯誤,展開巨集,包含頭文件等 b.編譯:*.c-->*.S c.彙編:*.S-->*.o d.鏈接:.o +庫文件=*.exe 1.2體驗在VC下程式的編譯 a.先編譯,在鏈接 b.修改了哪個文件,就單獨編譯此文件,在鏈接 c.修改了哪個 ...
  • 既然瞭解了IL的介面和動態類之間的知識,何不使用進來項目實驗一下呢?而第一反應就是想到了平時經常說的IOC容器,在園子里搜索了一下也有這類型的文章http://www.cnblogs.com/kklldog/p/3395641.html,借鑒一下前人的知識就來實現一下吧。IOC的概念就不介紹了,想了 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...