阿里云服务器免费领卷啦。

捡代码论坛-最全的游戏源码下载技术网站!

 找回密码
 立 即 注 册

QQ登录

只需一步,快速开始

搜索
关于源码区的附件失效或欺骗帖, 处理办法
查看: 2086|回复: 1

分享api接口验证模块

[复制链接]

4208

主题

210

回帖

12万

积分

管理员

管理员

Rank: 9Rank: 9Rank: 9

积分
126189
QQ
发表于 2016-12-28 10:47:55 | 显示全部楼层 |阅读模式
一.前言
  权限验证在开发中是经常遇到的,通常也是封装好的模块,如果我们是使用者,通常指需要一个标记特性或者配置一下就可以完成,但实际里面还是有许多东西值得我们去探究。有时候我们也会用一些开源的权限验证框架,不过能自己实现一遍就更好,自己开发的东西成就感(逼格)会更高一些。进入主题,本篇主要是介绍接口端的权限验证,这个部分每个项目都会用到,所以最好就是也把它插件化,放在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上的,定义如下
  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
  2. public class ApiAuthorizeAttribute : ApiBaseAuthorizeAttribute
  3. {
  4.     private static string[] _keys = new string[] { "appkey", "timestamp", "random", "sign" };

  5.     public override void OnAuthorization(AuthorizationContext context)
  6.     {
  7.         //是否允许匿名访问
  8.         if (context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false))
  9.         {
  10.             return;
  11.         }
  12.         HttpRequestBase request = context.HttpContext.Request;
  13.         string appkey = request[_keys[0]];
  14.         string timestamp = request[_keys[1]];
  15.         string random = request[_keys[2]];
  16.         string sign = request[_keys[3]];
  17.         ApiStanderConfig config = ApiStanderConfigProvider.Config;
  18.         if(string.IsNullOrEmpty(appkey))
  19.         {
  20.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissAppKey);
  21.             return;
  22.         }
  23.         if (string.IsNullOrEmpty(timestamp))
  24.         {
  25.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissTimeStamp);
  26.             return;
  27.         }
  28.         if (string.IsNullOrEmpty(random))
  29.         {
  30.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissRamdon);
  31.             return;
  32.         }
  33.         if(string.IsNullOrEmpty(sign))
  34.         {
  35.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.MissSign);
  36.             return;
  37.         }
  38.         //验证key
  39.         string secretKey = string.Empty;
  40.         if(!SecretKeyContainer.Container.TryGetValue(appkey, out secretKey))
  41.         {
  42.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.KeyNotFound);
  43.             return;
  44.         }
  45.         //验证时间戳(时间戳是指1970-1-1到现在的总秒数)     
  46.         long lt = 0;
  47.         if (!long.TryParse(timestamp, out lt))
  48.         {
  49.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.TimeStampTypeError);
  50.             return;
  51.         }
  52.         long now = DateTime.Now.Subtract(new DateTime(1970, 1, 1)).Ticks;
  53.         if (now - lt > new TimeSpan(0, config.Minutes, 0).Ticks)
  54.         {
  55.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.PastRequet);
  56.             return;
  57.         }
  58.         //验证签名
  59.         //httpmethod + url + 参数字符串 + timestamp + secreptkey
  60.         MD5Hasher md5 = new MD5Hasher();
  61.         string parameterStr = GenerateParameterString(request);
  62.         string url = request.Url.ToString();
  63.         url = url.Substring(0, url.IndexOf('?'));
  64.         string serverSign = md5.Hash(request.HttpMethod + url + parameterStr + timestamp + secretKey);
  65.         if(sign != serverSign)
  66.         {
  67.             SetUnAuthorizedResult(context, ApiUnAuthorizeType.ErrorSign);
  68.             return;
  69.         }
  70.     }

  71.     private string GenerateParameterString(HttpRequestBase request)
  72.     {
  73.         string parameterStr = string.Empty;
  74.         var collection = request.HttpMethod == "GET" ? request.QueryString : request.Form;
  75.         foreach(var key in collection.AllKeys.Except(_keys))
  76.         {
  77.             parameterStr += key + collection[key] ?? string.Empty;
  78.         }
  79.         return parameterStr;
  80.     }
  81. }
复制代码
 下面会对这段核心代码进行解析。ApiStanderConfig包装了一些配置信息,例如上面我们说到的过期时间是20分钟,但我们希望可以在模块外部进行自定义。所以通过一个ApiStanderConfig来包装,通过ApiStanderConfigProvider来注册和获取。ApiStanderConfig和ApiStanderConfigProvider的定义如下

  1. public class ApiStanderConfig
  2. {
  3.     public int Minutes { get; set; }
  4. } 
  5. public class ApiStanderConfigProvider
  6. {
  7.     public static ApiStanderConfig Config { get; private set; }

  8.     static ApiStanderConfigProvider()
  9.     {
  10.         Config = new ApiStanderConfig()
  11.         {
  12.             Minutes = 20
  13.         };
  14.     }

  15.     public static void Register(ApiStanderConfig config)
  16.     {
  17.         Config = config;
  18.     }
  19. }
复制代码
 前面介绍到服务端会维护一个appkey-secretkey的集合,这里通过一个SecretKeyContainer实现,它的Container就是一个字典集合,定义如下
  1. public class SecretKeyContainer
  2. {
  3.     public static Dictionary<string, string> Container { get; private set; }

  4.     static SecretKeyContainer()
  5.     {
  6.         Container = new Dictionary<string, string>();
  7.     }

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

  12.     public static void Register(Dictionary<string, string> set)
  13.     {
  14.         foreach(var key in set)
  15.         {
  16.             Container.Add(key.Key, key.Value);
  17.         }
  18.     }
  19. }
复制代码
 可以看到,上面有很多的条件判断,并且错误会有不同的描述。所以我定义了一个ApiUnAuthorizeType错误类型枚举和DescriptionAttribute标记,如下:

  1. public enum ApiUnAuthorizeType
  2. {
  3.     [Description("时间戳类型错误")]
  4.     TimeStampTypeError = 1000,

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

  7.     [Description("时间戳缺失")]
  8.     MissTimeStamp = 1002,

  9.     [Description("随机数缺失")]
  10.     MissRamdon = 1003,

  11.     [Description("签名缺失")]
  12.     MissSign = 1004,

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

  15.     [Description("过期请求")]
  16.     PastRequet = 1006,

  17.     [Description("错误的签名")]
  18.     ErrorSign = 1007
  19. }
  20. public class DescriptionAttribute : Attribute
  21. {
  22.     public string Description { get; set; }

  23.     public DescriptionAttribute(string description)
  24.     {
  25.         Description = description;
  26.     }
  27. }
复制代码
  当验证不通过时,会调用SetUnAuthorizedResult,并且请求不需再进行下去了。这个方法是在基类中实现的,如下

  1. public class ApiBaseAuthorizeAttribute : AuthorizeAttribute
  2. {
  3.     protected virtual void SetUnAuthorizedResult(AuthorizationContext context, ApiUnAuthorizeType type)
  4.     {
  5.         UnAuthorizeHandlerProvider.ApiHandler(context, type);
  6.         HandleUnauthorizedRequest(context);
  7.     }

  8.     protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
  9.     {
  10.         if (filterContext.Result != null)
  11.         {
  12.             return;
  13.         }
  14.         base.HandleUnauthorizedRequest(filterContext);
  15.     }
  16. }
复制代码
  可以看到,它通过一个委托根据错误类型处理结果,UnAuthorizeHandlerProvider定义如下
  1. public class UnAuthorizeHandlerProvider
  2. {
  3.     public static Action<AuthorizationContext, ApiUnAuthorizeType> ApiHandler { get; private set; }

  4.     static UnAuthorizeHandlerProvider()
  5.     {
  6.         ApiHandler = ApiUnAuthorizeHandler.Handler;
  7.     }

  8.     public static void Register(Action<AuthorizationContext, ApiUnAuthorizeType> action)
  9.     {
  10.         ApiHandler = action;
  11.     }
  12. }   
复制代码
 它默认通过ApiUnAuthorizeHandler.Handler来处理结果,但也可以在模块外部进行注册。默认的处理为ApiUnAuthorizeHandler.Handler,如下
  1. public class ApiUnAuthorizeHandler
  2. {
  3.     public readonly static Action<AuthorizationContext, ApiUnAuthorizeType> Handler = (context, type) =>
  4.     {
  5.         context.Result = new StanderJsonResult()
  6.         {
  7.             Result = FastStatnderResult.Fail(type.GetDescription(), (int)type)
  8.         };
  9.     };
  10. }
复制代码

 它的操作就是返回一个json结果。type.GetDescription是一个扩展方法,目的就是获取DescriptionAttribute的描述信息,如下
  1. public static class EnumExt
  2. {
  3.     public static string GetDescription(this Enum e)
  4.     {
  5.         Type type = e.GetType();
  6.         var attributes = type.GetField(e.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
  7.         if(attributes.IsNullOrEmpty())
  8.         {
  9.             return null;
  10.         }
  11.         return attributes[0].Description;
  12.     }
  13. }
复制代码
 这里还涉及到几个json相关对象,但它们应该不影响阅读。StanderResult, FastStanderResult, StanderJsonResult,有兴趣也可以看一下,在实际项目中有很多地方都可以用到它们,可以标准和简化许多操作。如下
  1. public class StanderResult
  2. {
  3.     public bool IsSuccess { get; set; }

  4.     public object Data { get; set; }

  5.     public string Description { get; set; }

  6.     public int Code { get; set; }
  7. }

  8. public static class FastStatnderResult
  9. {
  10.     private static StanderResult _success = new StanderResult() { IsSuccess = true };

  11.     public static StanderResult Success()
  12.     {
  13.         return _success;
  14.     }

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

  19.     public static StanderResult Fail()
  20.     {
  21.         return new StanderResult() { IsSuccess = false };
  22.     }

  23.     public static StanderResult Fail(string description, int code = 0)
  24.     {
  25.         return new StanderResult() { IsSuccess = false, Description = description, Code = code };
  26.     }
  27. }  
复制代码
  1. public class StanderJsonResult : ActionResult
  2. {
  3.     public StanderResult Result { get; set; }

  4.     public string ContentType { get; set; }

  5.     public Encoding Encoding { get; set; }

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

  11.         if (Encoding != null)
  12.         {
  13.             response.ContentEncoding = Encoding;
  14.         }
  15.         string json = JsonConvert.SerializeObject(Result);
  16.         response.Write(json);
  17.     }
  18. }
复制代码
四.例子
  我们在程序初始化时注册appkey-secretkey,如
  1. //注册appkey-secretkey
  2. string[] appkey1 = ConfigurationReader.GetStringValue("appkey1").Split(',');
  3. SecretKeyContainer.Container.Add(appkey1[0], appkey1[1]);
复制代码
下面的使用就很简单了,标记需要验证的接口。如

  1. [ApiAuthorize]
  2. public ActionResult QueryBalance(int userId)
  3. {
  4.     return Json("查询成功");
  5. }
复制代码

  我们在网页输入链接测试:如
      1.输入过期时间会提示{"IsSuccess":false,"Data":null,"Description":"过期请求","Code":1006}
      2.输入错误签名会提示{"IsSuccess":false,"Data":null,"Description":"错误的签名","Code":1007}
  只有所有验证都成功时才可以访问。
  当然实际项目的验证可能会更复杂一些,条件也会更多一些,不过都可以在此基础上进行扩展。如上面所说,这种算法可以保证请求是合法的,而且参数不被篡改,但还是无法保证请求不被窃取,要实现更高的安全性还是需要使用https。


00


捡代码论坛-最全的游戏源码下载技术网站! - 论坛版权郑重声明:
1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
2、本站所有主题由该帖子作者发表,该帖子作者与捡代码论坛-最全的游戏源码下载技术网站!享有帖子相关版权
3、捡代码论坛版权,详细了解请点击。
4、本站所有内容均由互联网收集整理、网友上传,并且以计算机技术研究交流为目的,仅供大家参考、学习,不存在任何商业目的与商业用途。
5、若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。 我们不承担任何技术及版权问题,且不对任何资源负法律责任。
6、如无法链接失效或侵犯版权,请给我们来信:jiandaima@foxmail.com

回复

使用道具 举报

0

主题

76

回帖

714

积分

高级会员

Rank: 4

积分
714
发表于 2017-1-4 18:57:50 | 显示全部楼层
登录可见评论
回复

使用道具 举报

*滑块验证:
您需要登录后才可以回帖 登录 | 立 即 注 册

本版积分规则

技术支持
在线咨询
QQ咨询
3351529868

QQ|手机版|小黑屋|捡代码论坛-专业源码分享下载 ( 陕ICP备15015195号-1|网站地图

GMT+8, 2024-4-27 20:58

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表