一.前言

  权限验证在开发中是经常遇到的,通常也是封装好的模块,如果我们是使用者,通常指需要一个标记特性或者配置一下就可以完成,但实际里面还是有许多东西值得我们去探究。有时候我们也会用一些开源的权限验证框架,不过能自己实现一遍就更好,自己开发的东西成就感(逼格)会更高一些。进入主题,本篇主要是介绍接口端的权限验证,这个部分每个项目都会用到,所以最好就是也把它插件化,放在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。

分享api接口验证模块的更多相关文章

  1. 谈谈如何用eoLinker管理各类API接口及分享API接口管理小技巧教程

    在前后端分离的开发模式下,前后端往往需要接口文档来进行交互.我的上一篇随笔中已经写到用传统的文档写接口时,由于需求经常变动,接口文档也会随之变动.一开始,某接口信息已经写入文档,但后期因为需求变动,发 ...

  2. API接口验证

    一.前言 权限验证在开发中是经常遇到的,通常也是封装好的模块,如果我们是使用者,通常指需要一个标记特性或者配置一下就可以完成,但实际里面还是有许多东西值得我们去探究.有时候我们也会用一些开源的权限验证 ...

  3. api接口验证shal()

    就安全来说,所有客户端和服务器端的通信内容应该都要通过加密通道(HTTPS)传输,明文的HTTP通道将会是man-in-the- middle及其各种变种攻击的温床.所谓man-in-the-midd ...

  4. ASP.NET MVC API 接口验证

    项目中有一个留言消息接口,接收其他系统的留言和展示留言,参考了网上的一些API验证方法,发现使用通用权限管理系统提供的验证方法最完美(http://www.cnblogs.com/jirigala/p ...

  5. 手机端API接口验证及参数签名验证

    问题背景: 后端服务对手机APP端开放API,没有基本的校验就是裸奔,别人抓取接口后容易恶意请求,不要求严格的做的安全,但是简单的基础安全屏障是要建立的,再配合HTTPS使用,这样使后端服务尽可能的安 ...

  6. Api接口通用安全策略及实现-OSS.Core

    这篇文章一直说写,迟迟没有动手,这两天看到一些应用接口数据被别人爬虫.短信接口被人高频率请求攻击等案列,感觉简单概述分享一下接口安全验证还是有必要的.毕竟当下基本都以客户端应用为主,如果前期疏忽,发布 ...

  7. API 接口设计规范

    概述 这篇文章分享 API 接口设计规范,目的是提供给研发人员做参考. 规范是死的,人是活的,希望自己定的规范,不要被打脸. 路由命名规范 动作 前缀 备注 获取 get get{XXX} 获取 ge ...

  8. Node教程——API接口开发(MangoDB+Express)

    一.大纲 大纲: 关于架构, 首先我们的有一个app.js这个就是根路由起点,用来最初的打入口 它的功能有: 1.1 引入模块创建基础的网站服务器, 1.2 导入bodyPasser,过滤还有处理我们 ...

  9. API接口防止参数篡改和重放攻击

    {近期领导要求我对公司业务的支付类的ocr接口做研究,是否存在支付接口重放攻击,so.....} API重放攻击(Replay Attacks)又称重播攻击.回放攻击.他的原理就是把之前窃听到的数据原 ...

随机推荐

  1. 将 instance 部署到 OVS Local Network - 每天5分钟玩转 OpenStack(130)

    上一节创建了 OVS 本地网络 first_local_net,今天我们会部署一个 instance 到该网络并分析网络结构.launch 一个 instance,选择 first_local_net ...

  2. MJRefresh 源码解读 + 使用

    MJRefresh这个刷新控件是一款非常好用的框架,我们在使用一个框架的同时,最好能了解下它的实现原理,不管是根据业务要求在原有的基础上修改代码,还是其他的目的,弄明白作者的思路和代码风格,会受益匪浅 ...

  3. 深入node之Transform

    Transform流特性 在开发中直接接触Transform流的情况不是很多,往往是使用相对成熟的模块或者封装的API来完成流的处理,最为特殊的莫过于through2模块和gulp流操作.那么,Tra ...

  4. CSharpGL(33)使用uniform块来优化对uniform变量的读写

    CSharpGL(33)使用uniform块来优化对uniform变量的读写 +BIT祝威+悄悄在此留下版了个权的信息说: Uniform块 如果shader程序变得比较复杂,那么其中用到的unifo ...

  5. Newtonsoft.Json设置类的属性不序列化

    参考页面: http://www.yuanjiaocheng.net/webapi/parameter-binding.html http://www.yuanjiaocheng.net/webapi ...

  6. SMBus set up a 2-byte EEPROM address for read/write

    Sequencer Engine spec: http://www.analog.com/media/en/technical-documentation/data-sheets/ADM1260.pd ...

  7. java.IO输入输出流:过滤流:buffer流和data流

    java.io使用了适配器模式装饰模式等设计模式来解决字符流的套接和输入输出问题. 字节流只能一次处理一个字节,为了更方便的操作数据,便加入了套接流. 问题引入:缓冲流为什么比普通的文件字节流效率高? ...

  8. Android快乐贪吃蛇游戏实战项目开发教程-06虚拟方向键(五)绘制方向键箭头

    本系列教程概述与目录:http://www.cnblogs.com/chengyujia/p/5787111.html本系列教程项目源码GitHub地址:https://github.com/jack ...

  9. 免费SSL证书 之Let’s Encrypt申请与部署(Windows Nginx)

    我着着皇帝的新衣,但是你看不见    有一颗愿意等待的心,说明你对未来充满希望.有一颗充满希望的心,那么等待又算什么.人就是在等待与希望中度过,我们永远要对未来充满信心! 读在最前面: 1.本文案例为 ...

  10. .Net中的AOP系列之《拦截位置》

    返回<.Net中的AOP>系列学习总目录 本篇目录 位置拦截 .Net中的字段和属性 PostSharp位置拦截 真实案例--懒加载 .Net中的懒加载 使用AOP实现懒加载 如何懒加载字 ...