zoukankan      html  css  js  c++  java
  • 分享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。

  • 相关阅读:
    手把手教你利用create-nuxt-app脚手架创建NuxtJS应用
    初识NuxtJS
    webpack打包Vue应用程序流程
    用选择器代替表格列的筛选功能
    Element-UI
    Spectral Bounds for Sparse PCA: Exact and Greedy Algorithms[贪婪算法选特征]
    Sparse Principal Component Analysis via Rotation and Truncation
    Generalized Power Method for Sparse Principal Component Analysis
    Sparse Principal Component Analysis via Regularized Low Rank Matrix Approximation(Adjusted Variance)
    Truncated Power Method for Sparse Eigenvalue Problems
  • 原文地址:https://www.cnblogs.com/4littleProgrammer/p/6220351.html
Copyright © 2011-2022 走看看