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上的,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
[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的定义如下

1
2
3
4
public  class  ApiStanderConfig
{
     public  int  Minutes {  get set ; }
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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就是一个字典集合,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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标记,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
}
1
2
3
4
5
6
7
8
9
public  class  DescriptionAttribute : Attribute
{
     public  string  Description {  get set ; }
 
     public  DescriptionAttribute( string  description)
     {
         Description = description;
     }
}

  当验证不通过时,会调用SetUnAuthorizedResult,并且请求不需再进行下去了。这个方法是在基类中实现的,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,如下

1
2
3
4
5
6
7
8
9
10
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的描述信息,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
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,有兴趣也可以看一下,在实际项目中有很多地方都可以用到它们,可以标准和简化许多操作。如下

四.例子

  我们在程序初始化时注册appkey-secretkey,如

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

  下面的使用就很简单了,标记需要验证的接口。如

1
2
3
4
5
[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。

转载于:https://www.cnblogs.com/zhangxiaolei521/p/6222790.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值