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

分享api接口驗證模塊,api接口模塊

編輯:關於.NET

分享api接口驗證模塊,api接口模塊


一.前言

  權限驗證在開發中是經常遇到的,通常也是封裝好的模塊,如果我們是使用者,通常指需要一個標記特性或者配置一下就可以完成,但實際裡面還是有許多東西值得我們去探究。有時候我們也會用一些開源的權限驗證框架,不過能自己實現一遍就更好,自己開發的東西成就感(逼格)會更高一些。進入主題,本篇主要是介紹接口端的權限驗證,這個部分每個項目都會用到,所以最好就是也把它插件化,放在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。

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