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之大杂绘