Nancy之实现API的功能

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:

 1 using Catcher.API.Helpers;
2 namespace Catcher.API
3 {
4 /// <summary>
5 /// the entity of route parameters
6 /// </summary>
7 public class UrlParaEntity
8 {
9 public string Type { get; set; }
10 public string PageIndex { get; set; }
11 public string PageSize { get; set; }
12 public string Sign { get; set; }
13 /// <summary>
14 /// the key
15 /// </summary>
16 const string encryptKey = "c1a2t3c4h5e6r.";
17 /// <summary>
18 /// validate the parameters
19 /// </summary>
20 /// <returns></returns>
21 public bool Validate()
22 {
23 return this.Sign == EncryptHelper.GetEncryptResult((Type + PageIndex + PageSize),encryptKey);
24 }
25 }
26 }
实体里面包含了一个校验的方法来判断参数是否有被篡改。sign参数的加密规则是:把type、pageindex、pagesize三个参数

拼接起来,并加上私钥来加密。这里为了偷懒,私钥直接在代码里了写死了。正常情况下应该将私钥存放在数据库中的,有一个key与之对应。

下面就是A、B公司协商好的加密算法了。

这里采用的加密算法是:HMACMD5 ,它所在的命名空间是system.security.cryptography

 1 using System.Security.Cryptography;
2 using System.Text;
3 namespace Catcher.API.Helpers
4 {
5 public class EncryptHelper
6 {
7 /// <summary>
8 /// HMACMD5 encrypt
9 /// </summary>
10 /// <param name="data">the date to encrypt</param>
11 /// <param name="key">the key used in HMACMD5</param>
12 /// <returns></returns>
13 public static string GetEncryptResult(string data, string key)
14 {
15 HMACMD5 source = new HMACMD5(Encoding.UTF8.GetBytes(key));
16 byte[] buff = source.ComputeHash(Encoding.UTF8.GetBytes(data));
17 string result = string.Empty;
18 for (int i = 0; i < buff.Length; i++)
19 {
20 result += buff[i].ToString("X2"); // hex format
21 }
22 return result;
23 }
24 }
25 }

基本的东西已经有了,下面就是要怎么去开发API了。

既然前面提到了要校验,那么,我们在那里做校验呢?

是在方法里面做校验吗?这个太不灵活,可能后面会改的很痛苦。DRY嘛,还是要遵守一下的。

用过mvc都会知道,验证某个用户是否有权限访问某页面,常规的做法就是用authorizeattribute

在Nancy中,我是在BeforePipeline中来实现这个校验。

BeforePipeline是什么呢,可以说和mvc中的那个application_beginrequest方法类似!

稍微具体一点的可以看看我之前的博客 (Nancy之Pipelines三兄弟(Before After OnError))。

 1 using Nancy;
2 using Nancy.ModelBinding;
3 namespace Catcher.API
4 {
5 public class BaseOpenAPIModule : NancyModule
6 {
7 public BaseOpenAPIModule()
8 {
9 }
10 public BaseOpenAPIModule(string modulePath)
11 : base(modulePath)
12 {
13 Before += TokenValidBefore;
14 }
15 /// <summary>
16 /// validate the parameters in before pipeline
17 /// </summary>
18 /// <param name="context"></param>
19 /// <returns></returns>
20 private Response TokenValidBefore(NancyContext context)
21 {
22 //to bind the parameters of the route parameters
23 var para = this.Bind<UrlParaEntity>();
24 //if pass the validate return null
25 return !para.Validate() ? Response.AsText("I think you are a bad man!!") : null;
26 }
27 }
28 }

要注意的是这个类要继承NancyModule,这个是根!!就像在MVC中,每一个控制器都要继承Controller一样!

其中的TokenValidBefore方法是关键,通过得到参数实体,调用实体的校验方法去判断,通过就返回null,不通过就给一个提示信息。

这里还是比较简单的做法。适合的场景是仅仅提供少量的接口方法。因为方法一多,不能确保传输的参数名称一致,

那么在bind的时候就会出问题。当然为不同的接口提供一个实体,也是一个不为过的方法。

下面就是Module中的返回数据了。

 1 using Nancy;
2 using System.Collections.Generic;
3 namespace Catcher.API
4 {
5 public class OpenProductAPI : BaseOpenAPIModule
6 {
7 public OpenProductAPI() : base ("/product")
8 {
9 Get["/"] = _ =>
10 {
11 var list = new List<Product>()
12 {
13 new Product { Id=1, Name="p1", Type="t1", Price=12.9m, OtherProp="" },
14 new Product { Id=2, Name="p2", Type="t2", Price=52.9m, OtherProp="remark" }
15 };
16 //response the json value
17 return Response.AsJson(list);
18 //response the xml value
19 //return Response.AsXml(list);
20 };
21 }
22 }
23 }

这里的代码是最简单的,只是单纯的返回数据就是了!不过要注意的是,这个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,并没有涉及哈希等其他类型:

 1 using StackExchange.Redis;
2 using System;
3 using Newtonsoft.Json;
4 namespace Catcher.API.Helpers
5 {
6 public class RedisCacheHelper
7 {
8 /// <summary>
9 /// get the connection string from the config
10 /// </summary>
11 private static string _connstr = System.Configuration.ConfigurationManager.AppSettings["redisConnectionString"];
12 /// <summary>
13 /// instance of the <see cref="ConnectionMultiplexer"/>
14 /// </summary>
15 private static ConnectionMultiplexer _conn = ConnectionMultiplexer.Connect(_connstr);
16 /// <summary>
17 /// the database of the redis
18 /// </summary>
19 private static IDatabase _db = _conn.GetDatabase();
20 /// <summary>
21 /// set the string cache
22 /// </summary>
23 /// <param name="key">Key of Redis</param>
24 /// <param name="value">value of the key</param>
25 /// <param name="expiry">expiry time</param>
26 /// <returns>true/false</returns>
27 public static bool Set(string key, string value, TimeSpan? expiry = default(TimeSpan?))
28 {
29 return _db.StringSet(key, value, expiry);
30 }
31 /// <summary>
32 /// set the entity cache
33 /// </summary>
34 /// <typeparam name="T">type of the obj</typeparam>
35 /// <param name="key">key of redis</param>
36 /// <param name="obj">value of the key</param>
37 /// <param name="expiry">expiry time</param>
38 /// <returns>true/false</returns>
39 public static bool Set<T>(string key, T obj, TimeSpan? expiry = default(TimeSpan?))
40 {
41 string json = JsonConvert.SerializeObject(obj);
42 return _db.StringSet(key, json, expiry);
43 }
44 /// <summary>
45 /// get the value by the redis key
46 /// </summary>
47 /// <param name="key">Key of Redis</param>
48 /// <returns>value of the key</returns>
49 public static RedisValue Get(string key)
50 {
51 return _db.StringGet(key);
52 }
53 /// <summary>
54 /// get the value by the redis key
55 /// </summary>
56 /// <typeparam name="T">type of the entity</typeparam>
57 /// <param name="key">key of redis</param>
58 /// <returns>entity of the key</returns>
59 public static T Get<T>(string key)
60 {
61 if (!Exist(key))
62 {
63 return default(T);
64 }
65 return JsonConvert.DeserializeObject<T>(_db.StringGet(key));
66 }
67 /// <summary>
68 /// whether the key exist
69 /// </summary>
70 /// <param name="key">key of redis</param>
71 /// <returns>true/false</returns>
72 public static bool Exist(string key)
73 {
74 return _db.KeyExists(key);
75 }
76 /// <summary>
77 /// remove the cache by the key
78 /// </summary>
79 /// <param name="key"></param>
80 /// <returns></returns>
81 public static bool Remove(string key)
82 {
83 return _db.KeyDelete(key);
84 }
85 }
86 }

然后就是修改我们的BaseOpenAPIModule,把这个次数限制加上去。修改过后的代码如下:

 1 using Nancy;
2 using Nancy.ModelBinding;
3 using Catcher.API.Helpers;
4 using System;
5 using System.Configuration;
6 namespace Catcher.API
7 {
8 public class BaseOpenAPIModule : NancyModule
9 {
10 public BaseOpenAPIModule()
11 {
12 }
13 public BaseOpenAPIModule(string modulePath)
14 : base(modulePath)
15 {
16 Before += TokenValidBefore;
17 }
18 /// <summary>
19 /// validate the parameters in before pipeline
20 /// </summary>
21 /// <param name="context">the nancy context</param>
22 /// <returns></returns>
23 private Response TokenValidBefore(NancyContext context)
24 {
25 string ipAddr = context.Request.UserHostAddress;
26 if (IsIPUpToLimit(ipAddr))
27 return Response.AsText("up to the limit");
28
29 //to bind the parameters of the route parameters
30 var para = this.Bind<UrlParaEntity>();
31 //if pass the validate return null
32 return !para.Validate() ? Response.AsText("I think you are a bad man!!") : null;
33 }
34 /// <summary>
35 /// whether the ip address up to the limited count
36 /// </summary>
37 /// <param name="ipAddr">the ip address</param>
38 /// <returns>true/false</returns>
39 private bool IsIPUpToLimit(string ipAddr)
40 {
41 bool flag = false;
42 //end of the day
43 DateTime endTime = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd 23:59:59"));
44 TimeSpan seconds = endTime - DateTime.Now;
45 //first or not
46 if (RedisCacheHelper.Exist(ipAddr))
47 {
48 int count = (int)RedisCacheHelper.Get(ipAddr);
49 if (count < int.Parse(ConfigurationManager.AppSettings["limitCount"].ToString()))
50 RedisCacheHelper.Set(ipAddr, count + 1, TimeSpan.FromSeconds(seconds.TotalSeconds));
51 else
52 flag = true;
53 }
54 else
55 {
56 RedisCacheHelper.Set(ipAddr, 1, TimeSpan.FromSeconds(seconds.TotalSeconds));
57 }
58 return flag;
59 }
60 }
61 }

这里添加了一个方法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文档

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

  2. Nancy之实现API的功能

    0x01.前言 现阶段,用来实现API的可能大部分用的是ASP.NET Web API或者是ASP.NET MVC,毕竟是微软官方出产的,用的人也多. 但是呢,NancyFx也是一个很不错的选择.毕竟 ...

  3. Nancy之大杂烩

    Nancy关于Hosting的简单介绍 一. Nancy之基于Nancy.Hosting.Aspnet的小Demo 二.Nancy之基于Nancy.Hosting.Self的小Demo 三.Nancy ...

  4. 谈谈Nancy中让人又爱又恨的Diagnostics【上篇】

    前言 在Nancy中有个十分不错的功能-Diagnostics,可以说这个功能让人又爱又恨. 或许我们都做过下面这样的一些尝试: 记录某一个功能用到的相关技术信息 记录下网站的访问记录 全局配置某些框 ...

  5. 从 MVC 到使用 ASP.NET Core 6.0 的最小 API

    从 MVC 到使用 ASP.NET Core 6.0 的最小 API https://benfoster.io/blog/mvc-to-minimal-apis-aspnet-6/ 2007 年,随着 ...

  6. 浅析如何在Nancy中使用Swagger生成API文档

    前言 上一篇博客介绍了使用Nancy框架内部的方法来创建了一个简单到不能再简单的Document.但是还有许许多多的不足. 为了能稍微完善一下这个Document,这篇引用了当前流行的Swagger, ...

  7. 用Web api /Nancy 通过Owin Self Host简易实现一个 Http 服务器

    过去做 端游的Http 服务器 用的WebApi 或者Mvc架构,都是放在iis...而我已经是懒出一个地步,并不想去配iis,或者去管理iis,所以我很喜欢 Self host 的启动方式. C#做 ...

  8. 第二章 Rest框架 Nancy

    正如你看到的,Nancy有两个主要用途. 其中第一项是作为一种通用的基于 REST 框架,可替代 ASP.NET Web API 或其他Rest工具包. 默认情况下,Nancy提供一流的路由和内容协商 ...

  9. 在Linux中运行Nancy应用程序

    最近在研究如何将.NET应用程序移植到非Windows操作系统中运行,逐渐会写一些文章出来.目前还没有太深的研究,所以这些文章大多主要是记录我的一些实验. 这篇文章记录了我如何利用NancyFx编写一 ...

随机推荐

  1. vb.net常用函数

    当然,这些都可以从MSDN查到,但是有时候打开帮助老慢的,所以先放到这里放一放,查个函数什么的比较快一点.都是从网上搜来的.Abs(number) 取得数值的绝对值. Asc(String) 取得字符 ...

  2. hive 配置MySQL库

    chkconfig mysqld on MySQL开机自启动 建库: --hive数据库2create database hive DEFAULT CHARSET utf8 COLLATE utf8_ ...

  3. 管理node_modules

    http://stackoverflow.com/questions/15225865/centralise-node-modules-in-project-with-subproject

  4. [置顶] woff格式字体怎么打开和编辑?

    如题! woff百度百科:http://baike.baidu.com/link?url=toS7yqpN9VlEcO2GOEp5JEA9-TeaZgIdVqTOv7iHshsNvk-V8HtxEY0 ...

  5. 关于arguments.callee的用途

    arguments为js函数中两个隐藏属性中的一个(另一个为this) arguments表示所有传入的参数,为类数组(array-like)类型,arguments.length表示传入参数的长度, ...

  6. 加载loading的ajax写法

    ajaxStart()与ajaxStop():当Ajax请求开始时,会触发ajaxStart()方法的回调函数.当Ajax请求结束时,会触发ajaxStop()方法的回调函数.这些方法都是全局的方法, ...

  7. 在Windows的CMD中如何设置支持UTF8编码

    这个问题很多人开始都会不知道,当然包括曾经的我,当用到的时候,只好求助于伟大的股沟和度娘了.网上有设置的方法,但说明确不够详细系统,说设置字体为:Lucida Console.问题是,在默认方式下,只 ...

  8. WPF的重要新概念

    原文 http://www.cnblogs.com/free722/archive/2011/11/12/2238654.html 逻辑树与可视树 XAML天生就是用来呈现用户界面的,这是由于它具有层 ...

  9. 解决Robotium测试用例crash问题

    今天遇到一个棘手的问题 用robotium框架真机测试客户端时 跑到一半会crash 搜了一堆资料终于解决了 我的程序引起crash主要原因有两个: 1.用Robotium测试框架跑多个用例(写在同一 ...

  10. poj2141---字符串转换

    #include <stdio.h> #include <stdlib.h> int main() { ]; char tmp; scanf("%s",ke ...