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公司发出请求带的参数有:

type            ----产品类型
pageindex ----页码
pagesize    ----每页显示多少产品
sign         ----至关重要的签名参数

通过这些参数,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);
}
}
}
实体里面包含了一个校验的方法来判断参数是否有被篡改。sign参数的加密规则是:把type、pageindex、pagesize三个参数

拼接起来,并加上私钥来加密。这里为了偷懒,私钥直接在代码里了写死了。正常情况下应该将私钥存放在数据库中的,有一个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的写法也在注释中有提到。

到这里,这个简单的接口已经能够正常运行了,下面来看看效果吧:

正确无误的访问链接如下:

http://localhost:62933/product?type=type&pageindex=1&pagesize=2&sign=99186B4B5E923B4631B3E5DAC4134C4D

我们修改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的功能的更多相关文章

  1. Nancy之实现API

    Nancy之实现API的功能 0x01.前言 现阶段,用来实现API的可能大部分用的是ASP.NET Web API或者是ASP.NET MVC,毕竟是微软官方出产的,用的人也多. 但是呢,Nancy ...

  2. 浅析如何在Nancy中生成API文档

    前言 前后端分离,或许是现如今最为流行开发方式,包括UWP.Android和IOS这样的手机客户端都是需要调用后台的API来进行数据的交互. 但是这样对前端开发和APP开发就会面临这样一个问题:如何知 ...

  3. 对TControl和TWinControl相同与不同之处的深刻理解(每一个WinControl就相当于扮演了整个Windows的窗口管理角色,主要是窗口显示和窗口大小)——TWinControl就两个作用(管理子控件的功能和调用句柄API的功能)

    TControl是图形控件,它本身没有句柄,所以不能直接使用WINAPI显示,调整位置,发消息等等,只能想办法间接取得想要的效果,但是可以直接使用一些不需要句柄的API,比如InvalidateRec ...

  4. Windows加密API的功能分类

    本地数据加密保护本地数据加密保护机制提供了简单的DAPI调用接口,密钥管理等等一概由系统来处理.DAPI的数据加密保护机制在用户登录会话范围或者本地计算范围,使用操作系统设计的方式加密保护数据和解密还 ...

  5. 即将到来的Autodesk 主要产品2015版 产品和API新功能在线培训(免费)

    一年一度的Autodesk主要产品和API在线培训课程在5月份即将開始.我们呈献给大家5个课程. 1. Revit 2015 产品新功能及API 概览 2. Vault 2015产品新功能及API 概 ...

  6. eclipse(STS)安装jd-eclipse插件实现查看API源代码功能

    emmm,IDEA确实是比STS智能很多,不过适当的转化也是需要的,这里介绍一下eclipse(STS)实现查看class反编译的源文件的功能 去Java Decompiler官网下一下eclipse ...

  7. 13 Tensorflow API主要功能

    要想使用Tensorflow API,首先要知道它能干什么.Tensorflow具有Python.C++.Java.Go等多种语言API,其中Python的API是最简单和好用的. Tensor Tr ...

  8. 调用百度翻译API接口功能

    public string appid = "自己的APPID"; public string q = "要翻译的文本"; "; public str ...

  9. asp.net web api 授权功能

    1.重写授权方法 using System; using System.Collections.Generic; using System.Linq; using System.Net; using ...

随机推荐

  1. [.NET] 怎样使用 async & await 一步步将同步代码转换为异步编程

    怎样使用 async & await 一步步将同步代码转换为异步编程 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6079707.html  ...

  2. 香蕉云APP,2016下半年开发日记

    2016-6-17  数据库设计不应该过多依赖范式,适度的冗余可以加快搜索速度,在服务器的配置还可以的情况下,可以采用冗余来解决查找慢的问题.还一个是要选择好数据库引擎,例如 InnoDB 和 myi ...

  3. History API与浏览器历史堆栈管理

    移动端开发在某些场景中有着特殊需求,如为了提高用户体验和加快响应速度,常常在部分工程采用SPA架构.传统的单页应用基于url的hash值进行路由,这种实现不存在兼容性问题,但是缺点也有--针对不支持o ...

  4. 【Python五篇慢慢弹】快速上手学python

    快速上手学python 作者:白宁超 2016年10月4日19:59:39 摘要:python语言俨然不算新技术,七八年前甚至更早已有很多人研习,只是没有现在流行罢了.之所以当下如此盛行,我想肯定是多 ...

  5. Spring resource bundle多语言,单引号format异常

    Spring resource bundle多语言,单引号format异常 前言 十一假期被通知出现大bug,然后发现是多语言翻译问题.法语中有很多单引号,单引号在format的时候出现无法匹配问题. ...

  6. Java compiler level does not match解决方法

    从别的地方导入一个项目的时候,经常会遇到eclipse/Myeclipse报Description  Resource Path Location Type Java compiler level d ...

  7. eclipse如何添加Memory Analyzer

    ①启动Eclipse,并打开"Install New software..."对话框: ②点击Add,如图: ③点击OK,最后一直点next,完成

  8. PHP设计模式(七)适配器模式(Adapter For PHP)

    适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作. 如下图(借图): // 设置书的接口 // 书接口 interface BookI ...

  9. sqlServer去除字符串空格

    说起去除字符串首尾空格大家肯定第一个想到trim()函数,不过在sqlserver中是没有这个函数的,却而代之的是ltrim()和rtrim()两个函数.看到名字所有人都 知道做什么用的了,ltrim ...

  10. Pramp mock interview (4th practice): Matrix Spiral Print

    March 16, 2016 Problem statement:Given a 2D array (matrix) named M, print all items of M in a spiral ...