0x01、前言
现阶段,用来实现API的可能大部分用的是ASP.NET Web API或者是ASP.NET MVC,毕竟是微软官方出产的,用的人也多。
但是呢,NancyFx也是一个很不错的选择。毕竟人家的官方文档都是这样写的:framework for building HTTP based services。
本文主要是通过一个简单的场景和简单的实现来说明。
0x02、场景假设与分析
现在A公司与B公司有一些业务上的合作,B公司要得到一些关于A公司产品的信息
所以,A公司需要提供一个简单的接口去给B公司调用,从而获得公司的产品信息。
那么,问题来了,这是A公司提供的一个对外接口,那这个接口是任何人都可以访问吗?
是可以无限制的访问吗?有人闲着没事一直访问这个接口怎么办?
很明显,这个接口是A公司专门提供给B公司用的,所以要想方设法禁止其他人访问,不然A公司的信息就要。。。
当然,像这种类型的接口,常规的做法基本上就是用签名去检验传递的参数是否被篡改过。
比如像这样一个api
http:api.example.com/getall?p1=a&p2=b&sign=sign
带了三个参数,p1,p2,sign,其中sign这个值是由p1和p2来决定的
可以是这两个参数拼接在一起,再经过某种加密得到的一个值
也可以是这两个参数加上一个双方约定的私钥,再经过某种加密得到的一个值
也可以是增加一个时间戳得到三个参数再加上双方约定的私钥,经过某种加密得到的一个值
也可以是在时间戳的基础上加一个随机数得到四个参数再加上双方约定的私钥,经过某种加密得到的一个值
本文采取的是第二种,加一个双方的私钥。至于加时间戳和随机数也是同样的道理。
现在A、B公司约定他们的私钥为:c1a2t3c4h5e6r.
并且B公司向A公司发出请求带的参数有:
通过这些参数,B公司就可以得到一些A公司的产品信息了
这就就意味着 B公司请求数据的地址就是 :
http://api.a-company.com/getproduct?type=xxx&pageindex=xx&pagesize=xxx&sign=xxx
一般情况下,两个公司商讨完毕后就会产生一份详细的API文档
这份文档会包含请求的每个参数的要求,如长度限制、加密方法、如何加密等,以及返回的数据格式等等
这个时候,A公司就会照着这份文档进行开发。
下面就是设计开发阶段了
0x03、设计与实现
既然已经知道了要传输的参数,那么就先建立一个路由的参数实体UrlParaEntity:
using Catcher.API.Helpers;
namespace Catcher.API
{
/// <summary>
/// the entity of route parameters
/// </summary>
public class UrlParaEntity
{
public string Type { get; set; }
public string PageIndex { get; set; }
public string PageSize { get; set; }
public string Sign { get; set; }
/// <summary>
/// the key
/// </summary>
const string encryptKey = "c1a2t3c4h5e6r.";
/// <summary>
/// validate the parameters
/// </summary>
/// <returns></returns>
public bool Validate()
{
return this.Sign == EncryptHelper.GetEncryptResult((Type + PageIndex + PageSize),encryptKey);
}
}
}
拼接起来,并加上私钥来加密。这里为了偷懒,私钥直接在代码里了写死了。正常情况下应该将私钥存放在数据库中的,有一个key与之对应。
下面就是A、B公司协商好的加密算法了。
这里采用的加密算法是:HMACMD5 ,它所在的命名空间是system.security.cryptography
using System.Security.Cryptography;
using System.Text;
namespace Catcher.API.Helpers
{
public class EncryptHelper
{
/// <summary>
/// HMACMD5 encrypt
/// </summary>
/// <param name="data">the date to encrypt</param>
/// <param name="key">the key used in HMACMD5</param>
/// <returns></returns>
public static string GetEncryptResult(string data, string key)
{
HMACMD5 source = new HMACMD5(Encoding.UTF8.GetBytes(key));
byte[] buff = source.ComputeHash(Encoding.UTF8.GetBytes(data));
string result = string.Empty;
for (int i = ; i < buff.Length; i++)
{
result += buff[i].ToString("X2"); // hex format
}
return result;
}
}
}
基本的东西已经有了,下面就是要怎么去开发API了。
既然前面提到了要校验,那么,我们在那里做校验呢?
是在方法里面做校验吗?这个太不灵活,可能后面会改的很痛苦。DRY嘛,还是要遵守一下的。
用过mvc都会知道,验证某个用户是否有权限访问某页面,常规的做法就是用authorizeattribute
在Nancy中,我是在BeforePipeline中来实现这个校验。
BeforePipeline是什么呢,可以说和mvc中的那个application_beginrequest方法类似!
稍微具体一点的可以看看我之前的博客 (Nancy之Pipelines三兄弟(Before After OnError))。
using Nancy;
using Nancy.ModelBinding;
namespace Catcher.API
{
public class BaseOpenAPIModule : NancyModule
{
public BaseOpenAPIModule()
{
}
public BaseOpenAPIModule(string modulePath)
: base(modulePath)
{
Before += TokenValidBefore;
}
/// <summary>
/// validate the parameters in before pipeline
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private Response TokenValidBefore(NancyContext context)
{
//to bind the parameters of the route parameters
var para = this.Bind<UrlParaEntity>();
//if pass the validate return null
return !para.Validate() ? Response.AsText("I think you are a bad man!!") : null;
}
}
}
要注意的是这个类要继承NancyModule,这个是根!!就像在MVC中,每一个控制器都要继承Controller一样!
其中的TokenValidBefore方法是关键,通过得到参数实体,调用实体的校验方法去判断,通过就返回null,不通过就给一个提示信息。
这里还是比较简单的做法。适合的场景是仅仅提供少量的接口方法。因为方法一多,不能确保传输的参数名称一致,
那么在bind的时候就会出问题。当然为不同的接口提供一个实体,也是一个不为过的方法。
下面就是Module中的返回数据了。
using Nancy;
using System.Collections.Generic;
namespace Catcher.API
{
public class OpenProductAPI : BaseOpenAPIModule
{
public OpenProductAPI() : base ("/product")
{
Get["/"] = _ =>
{
var list = new List<Product>()
{
new Product { Id=, Name="p1", Type="t1", Price=12.9m, OtherProp="" },
new Product { Id=, Name="p2", Type="t2", Price=52.9m, OtherProp="remark" }
};
//response the json value
return Response.AsJson(list);
//response the xml value
//return Response.AsXml(list);
};
}
}
}
这里的代码是最简单的,只是单纯的返回数据就是了!不过要注意的是,这个Module并不是继承NancyModule
而是继承我们自己定义的BaseOpenAPIModule。
现在返回的数据格式主要有两种,JSON和XML,ASP.NET Web API 和 WCF也可以返回这两种格式的数据。
现在大部分应该是以JSON为主,所以示例也就用了Json,返回xml的写法也在注释中有提到。
到这里,这个简单的接口已经能够正常运行了,下面来看看效果吧:
正确无误的访问链接如下:
我们修改pagesize为3在访问就会有问题了!因为sign值是通过前面的三个参数生成的,改动之后,肯定是得不到想到的数据!
所以这就有效的预防了其他人窃取api返回的数据。
到这里,A公司的提出了个问题,这个接口在一天内是不是能够无限次访问?
of course not!!每天一个ip访问1000次都算多了吧!
那么,要如何来限制这个访问频率呢?
首先,要限制ip的访问次数,肯定要存储对应的ip的访问次数,这个毋庸置疑。
其次,每天都有一个上限,有个过期时间。
那么要怎么存储?用什么存储?这又是个问题!!
存数据库吧,用什么数据库呢?SQL Server ? MySql ? MongoDB ? Redis ?
好吧,我选 Redis 。key-value型数据库,再加上可以设置过期的时间,是比较符合我们的这个场景的。
演示这里的频率以天为单位,访问上限次数为10次(设的太多,我怕我的F5键要烂~~)
下面是具体的实现:
首先对Redis的操作简单封装一下,这里的封装只是针对string,并没有涉及哈希等其他类型:
using StackExchange.Redis;
using System;
using Newtonsoft.Json;
namespace Catcher.API.Helpers
{
public class RedisCacheHelper
{
/// <summary>
/// get the connection string from the config
/// </summary>
private static string _connstr = System.Configuration.ConfigurationManager.AppSettings["redisConnectionString"];
/// <summary>
/// instance of the <see cref="ConnectionMultiplexer"/>
/// </summary>
private static ConnectionMultiplexer _conn = ConnectionMultiplexer.Connect(_connstr);
/// <summary>
/// the database of the redis
/// </summary>
private static IDatabase _db = _conn.GetDatabase();
/// <summary>
/// set the string cache
/// </summary>
/// <param name="key">Key of Redis</param>
/// <param name="value">value of the key</param>
/// <param name="expiry">expiry time</param>
/// <returns>true/false</returns>
public static bool Set(string key, string value, TimeSpan? expiry = default(TimeSpan?))
{
return _db.StringSet(key, value, expiry);
}
/// <summary>
/// set the entity cache
/// </summary>
/// <typeparam name="T">type of the obj</typeparam>
/// <param name="key">key of redis</param>
/// <param name="obj">value of the key</param>
/// <param name="expiry">expiry time</param>
/// <returns>true/false</returns>
public static bool Set<T>(string key, T obj, TimeSpan? expiry = default(TimeSpan?))
{
string json = JsonConvert.SerializeObject(obj);
return _db.StringSet(key, json, expiry);
}
/// <summary>
/// get the value by the redis key
/// </summary>
/// <param name="key">Key of Redis</param>
/// <returns>value of the key</returns>
public static RedisValue Get(string key)
{
return _db.StringGet(key);
}
/// <summary>
/// get the value by the redis key
/// </summary>
/// <typeparam name="T">type of the entity</typeparam>
/// <param name="key">key of redis</param>
/// <returns>entity of the key</returns>
public static T Get<T>(string key)
{
if (!Exist(key))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(_db.StringGet(key));
}
/// <summary>
/// whether the key exist
/// </summary>
/// <param name="key">key of redis</param>
/// <returns>true/false</returns>
public static bool Exist(string key)
{
return _db.KeyExists(key);
}
/// <summary>
/// remove the cache by the key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static bool Remove(string key)
{
return _db.KeyDelete(key);
}
}
}
然后就是修改我们的BaseOpenAPIModule,把这个次数限制加上去。修改过后的代码如下:
using Nancy;
using Nancy.ModelBinding;
using Catcher.API.Helpers;
using System;
using System.Configuration;
namespace Catcher.API
{
public class BaseOpenAPIModule : NancyModule
{
public BaseOpenAPIModule()
{
}
public BaseOpenAPIModule(string modulePath)
: base(modulePath)
{
Before += TokenValidBefore;
}
/// <summary>
/// validate the parameters in before pipeline
/// </summary>
/// <param name="context">the nancy context</param>
/// <returns></returns>
private Response TokenValidBefore(NancyContext context)
{
string ipAddr = context.Request.UserHostAddress;
if (IsIPUpToLimit(ipAddr))
return Response.AsText("up to the limit"); //to bind the parameters of the route parameters
var para = this.Bind<UrlParaEntity>();
//if pass the validate return null
return !para.Validate() ? Response.AsText("I think you are a bad man!!") : null;
}
/// <summary>
/// whether the ip address up to the limited count
/// </summary>
/// <param name="ipAddr">the ip address</param>
/// <returns>true/false</returns>
private bool IsIPUpToLimit(string ipAddr)
{
bool flag = false;
//end of the day
DateTime endTime = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd 23:59:59"));
TimeSpan seconds = endTime - DateTime.Now;
//first or not
if (RedisCacheHelper.Exist(ipAddr))
{
int count = (int)RedisCacheHelper.Get(ipAddr);
if (count < int.Parse(ConfigurationManager.AppSettings["limitCount"].ToString()))
RedisCacheHelper.Set(ipAddr, count + , TimeSpan.FromSeconds(seconds.TotalSeconds));
else
flag = true;
}
else
{
RedisCacheHelper.Set(ipAddr, , TimeSpan.FromSeconds(seconds.TotalSeconds));
}
return flag;
}
}
}
这里添加了一个方法IsIPUpToLimit,这个方法通过从Redis中读取ip对应的值,并根据这个值来判断是否超过了上限。
这里的上限次数和redis的连接字符串都放在了appsettings里面,便于修改。
然后在TokenValidBefore方法中获取IP并做次数上限的判断。
下面是效果图
毕竟是要用的,不能在本地调试过了就结束了,还要上线的,说不定上线就会遇到问题的。
下面就结合TinyFox独立版在CentOS7上简单部署一下。
首先要在CentOS7上安装一下redis,具体的安装方法就不在这里说明了(下载源码,编译一下就可以了)。
启动之后如下(这里我换了个端口,没有用默认的):
然后将项目的配置文件的内容copy到tinyfox的配置文件中,这里主要是appsettings里面的redis连接字符串和上限次数
所以只需要把appsettings的内容贴过去就好了。
然后是简单的操作和效果图:
需要注意的是,StackExchange.Redis在mono上面是跑不起来的!
它会提示不能连接到Redis!!这真是不能忍。
不过我能跑起来就肯定有解决的方法啦~~StackExchange.Redis.Mono是可以在mono上跑的版本!!
而且只需要替换掉程序集就可以正常跑起来了。因为这个与StackExchange.Redis的程序集名称是一样的,所以不需要做其他的修改。还是很方便的
这里需要说明的是,在本地调试的时候,用的redis是windows版的,发布的时候才是用的linux版。
0x04、小结
在这个过程中,也是遇到了一些问题和疑惑。
问题的话主要就是windows独立版的tinyfox调试不成功,只能切换回通用版。
疑惑的话主要就是用Redis做这个次数的限制,是临时想的,不知道是否合理。
Web API 有一个开源的库,里面有这个对次数限制的拓展,有兴趣的可以看看
https://github.com/WebApiContrib/WebAPIContrib/tree/master/src/WebApiContrib
它里面用ConcurrentDictionary来实现了轻量级的缓存。
可能有人会问,ASP.NET MVC 、 ASP.NET Web API 、 NancyFx 之间是什么关系
下面说说我个人的看法(理解不一定正确,望指正):
MVC 很明显 包含了 M 、V、 C这三个部分
Web API 可以说是只包含了 M 、 C这两个部分
这里的话可以说Web API 是 MVC的一个子集,
所以说,web api能做的,mvc也能做,所以有许多公司是直接用mvc来开发接口的
NancyFx与Web API的话,并没有太大的关系
Web API 可以很容易的构建HTTP services,也是基于RESTful的
NancyFx 是基于HTTP的轻量级框架,也可以构建RESTful API。
硬要说有关系的话,那就是HTTP和RESTful。
NancyFx与MVC的话,也是没有太大的关系
但他们能算的上是两个好朋友,有着共同的兴趣爱好,能完成同样的事情
API,实现的方式有很多,怎么选择就看个人的想法了。
更多有关NancyFx的文章,可以移步到Nancy之大杂绘
Nancy之实现API的功能的更多相关文章
-
Nancy之实现API
Nancy之实现API的功能 0x01.前言 现阶段,用来实现API的可能大部分用的是ASP.NET Web API或者是ASP.NET MVC,毕竟是微软官方出产的,用的人也多. 但是呢,Nancy ...
-
浅析如何在Nancy中生成API文档
前言 前后端分离,或许是现如今最为流行开发方式,包括UWP.Android和IOS这样的手机客户端都是需要调用后台的API来进行数据的交互. 但是这样对前端开发和APP开发就会面临这样一个问题:如何知 ...
-
对TControl和TWinControl相同与不同之处的深刻理解(每一个WinControl就相当于扮演了整个Windows的窗口管理角色,主要是窗口显示和窗口大小)——TWinControl就两个作用(管理子控件的功能和调用句柄API的功能)
TControl是图形控件,它本身没有句柄,所以不能直接使用WINAPI显示,调整位置,发消息等等,只能想办法间接取得想要的效果,但是可以直接使用一些不需要句柄的API,比如InvalidateRec ...
-
Windows加密API的功能分类
本地数据加密保护本地数据加密保护机制提供了简单的DAPI调用接口,密钥管理等等一概由系统来处理.DAPI的数据加密保护机制在用户登录会话范围或者本地计算范围,使用操作系统设计的方式加密保护数据和解密还 ...
-
即将到来的Autodesk 主要产品2015版 产品和API新功能在线培训(免费)
一年一度的Autodesk主要产品和API在线培训课程在5月份即将開始.我们呈献给大家5个课程. 1. Revit 2015 产品新功能及API 概览 2. Vault 2015产品新功能及API 概 ...
-
eclipse(STS)安装jd-eclipse插件实现查看API源代码功能
emmm,IDEA确实是比STS智能很多,不过适当的转化也是需要的,这里介绍一下eclipse(STS)实现查看class反编译的源文件的功能 去Java Decompiler官网下一下eclipse ...
-
13 Tensorflow API主要功能
要想使用Tensorflow API,首先要知道它能干什么.Tensorflow具有Python.C++.Java.Go等多种语言API,其中Python的API是最简单和好用的. Tensor Tr ...
-
调用百度翻译API接口功能
public string appid = "自己的APPID"; public string q = "要翻译的文本"; "; public str ...
-
asp.net web api 授权功能
1.重写授权方法 using System; using System.Collections.Generic; using System.Linq; using System.Net; using ...
随机推荐
-
xamarin android ListView手动分组
xamarin的listview控件其实自带有分组方法,关于xamarin listview的自带分组方法请自行参考官方文档,我这里只写自己写的分组方法.xamarin自带的分组好是好,功能多,但是加 ...
-
关于web软件信息安全问题防护资料的整理 (一)
之前意识到了安全问题的重要性,于是就在网上找了一下安全问题的解决办法(主要是web应用以及*网站方面的),问了一下同学的公司是怎么保证安全的,跟我说用的是shiro安全机制这个貌似好多公司都在用,网 ...
-
高通平台FastMMI(FFBM模式)简介与进入方法
参考: http://blog.csdn.net/tfslovexizi/article/details/51499979 http://www.voidcn.com/blog/jimbo_lee/a ...
-
Yii源码阅读笔记(二十一)——请求处理流程
Yii2请求处理流程: 首先:项目路径/web/index.php (new yii\web\Application($config))->run();//根据配置文件创建App实例,先实例化y ...
-
设计模式的一些杂谈与反思---functionn和signals
以下关于GOF的一些例子命名不是很准确,但是大概意思差不多,懒得再去翻书了 模拟观察者模式 模拟中介者模式 模拟command模式 模拟memento和command 模拟观察者模式 观察者与职责 ...
-
mybatis中association的column传入多个参数值
顾名思义,association是联合查询. 在使用association中一定要注意几个问题.文笔不好,白话文描述一下. 1: <association property="fncg ...
-
Redshift扩容及踩到的坑
下午发现redshift集群已经没有什么空间了.删掉一些不须要的暂时表也仅仅降到86%左右,为了能放下这两天的数据必须扩容了 watermark/2/text/aHR0cDovL2Jsb2cuY3Nk ...
-
UNIX环境高级编程——pthread_create的问题
linux 下常用的创建多线程函数pthread_create(pthread_t * thread , pthread_attr_t * attr , void *(*start_routine)( ...
-
bzoj 1856
做这题之前先看道高考真题(好像是真题,我记不清了) 例:已知一个由n个0和n个1排列而成的数列,要求对于任意k∈N*且k∈[1,2n],在前k个数中1的个数不少于0的个数,求当n=4时这样的数列的数量 ...
-
javascript高级技巧篇(作用域安全、防篡改、惰性载入、节流、自定义事件,拖放)
安全的类型检测 在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式字符串.每个类在内部都有一个[[Class]]属性 ...