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×tamp=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-secretkeystring[] 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。
API接口验证的更多相关文章
- 分享api接口验证模块
一.前言 权限验证在开发中是经常遇到的,通常也是封装好的模块,如果我们是使用者,通常指需要一个标记特性或者配置一下就可以完成,但实际里面还是有许多东西值得我们去探究.有时候我们也会用一些开源的权限验证 ...
- api接口验证shal()
就安全来说,所有客户端和服务器端的通信内容应该都要通过加密通道(HTTPS)传输,明文的HTTP通道将会是man-in-the- middle及其各种变种攻击的温床.所谓man-in-the-midd ...
- ASP.NET MVC API 接口验证
项目中有一个留言消息接口,接收其他系统的留言和展示留言,参考了网上的一些API验证方法,发现使用通用权限管理系统提供的验证方法最完美(http://www.cnblogs.com/jirigala/p ...
- 手机端API接口验证及参数签名验证
问题背景: 后端服务对手机APP端开放API,没有基本的校验就是裸奔,别人抓取接口后容易恶意请求,不要求严格的做的安全,但是简单的基础安全屏障是要建立的,再配合HTTPS使用,这样使后端服务尽可能的安 ...
- API接口防止参数篡改和重放攻击
{近期领导要求我对公司业务的支付类的ocr接口做研究,是否存在支付接口重放攻击,so.....} API重放攻击(Replay Attacks)又称重播攻击.回放攻击.他的原理就是把之前窃听到的数据原 ...
- spring boot 2 集成JWT实现api接口认证
JSON Web Token(JWT)是目前流行的跨域身份验证解决方案.官网:https://jwt.io/本文使用spring boot 2 集成JWT实现api接口验证. 一.JWT的数据结构 J ...
- Web API接口 安全验证
在上篇随笔<Web API应用架构设计分析(1)>,我对Web API的各种应用架构进行了概括性的分析和设计,Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端 ...
- PHP开发api接口安全验证
php的api接口 在实际工作中,使用PHP写api接口是经常做的,PHP写好接口后,前台就可以通过链接获取接口提供的数据,而返回的数据一般分为两种情况,xml和json,在这个过程中,服务器并不知道 ...
- PHP开发api接口安全验证方法一
前台想要调用接口,需要使用几个参数生成签名.时间戳:当前时间随机数:随机生成的随机数 签名:特定方法生成的sign签名 算法规则在前后台交互中,算法规则是非常重要的,前后台都要通过算法规则计算出签名, ...
随机推荐
- i++问题
例题,以下代码的输出结果是什么? #include <stdio.h> int main() { ,,,,}; int *ptr = arr; *(ptr++) += ; printf(& ...
- 初次使用InstallShield笔记
公司的产品需要打包发布,以前一直没打包,打包方法的学习交给了我 下面的内容是我研究过后的,通过回忆写的笔记.由于不熟悉,可能会有错误或者是笨办法,希望高手指正,感激不尽. 有些东西是什么,以及是什么意 ...
- oracle修改序列
Oracle 序列(Sequence)主要用于生成流水号,在应用中经常会用到,特别是作为ID值,拿来做表主键使用较多. 但是,有时需要修改序列初始值(START WITH)时,有同仁使用这个语句来 ...
- Sphinx 的介绍和原理探索——不存储原始数据,原始数据来源于SQL,而生成索引放在内存或者磁盘中
摘自:http://blog.jobbole.com/101672/ What/Sphinx是什么 定义:Sphinx是一个全文检索引擎. 特性: 索引和性能优异 易于集成SQL和XML数据源,并可使 ...
- IT的灵魂是流程,流程的灵魂是业务,业务的灵魂是战略
IT的灵魂是流程,流程的灵魂是业务,业务的灵魂是战略.高效的IT平台不在于IT技术,而在于好的管理模式与流程设计 从以组织为核心转向以流程为核心 流程管理核心是从流程角度出发,关注流程是否增值,籍此建 ...
- Java:多线程<二> 同步
由于多线程的访问出现延迟和线程的随机性,在使用多线程时往往会伴随安全性的问题,这些问题一旦出现将会是非常严重的.为了解决这种安全性问题,synchronized出现了. synchronized用法一 ...
- java_easyui体系之DataGrid(4)[转]
一:简介 在前面DataGrid(3)的基础上添加后台的实现.本来是想只搭建前台页面的.后台不写.现在觉得还是都实现好点.从真实情况出发.后台用的ssh. 1. 新增冻结列功能. 2. 实现界面的添加 ...
- 二模 (15)day1
第一题: 题目大意: 有两个长度为N的序列A和B,在A和B中各任取一个数相加可以得到N2个和,求这N2个和中最小的N个. 解题过程: 1.这题是刘汝佳<<训练指南>>上的一道经 ...
- [vijos P1391] 想越狱的小杉
考前最后一题,竟然是第一次码SPFA,虽然这个算法早有耳闻,甚至在闻所未闻之前自己有过一个类似的想法,说白了就是广搜啊,但是敲起来还是第一次啊,而且这还不是真正意义上的SPFA. 完全按照自己想法来码 ...
- [vijos P1524] 最小监视代价
历时四天(本周三至本周六),本人的第一道网络流题目终于通过了…虽然这么慢才搞懂很大程度是因为脑子笨,但是还是要吐槽一下: (1)选的这道题吧居然是无向图,对于初学者我表示呵呵,昨晚到现在一直在纠结怎么 ...