Nancy之实现API的功能

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 原文:Nancy之实现API的功能0x01、前言 现阶段,用来实现API的可能大部分用的是ASP.NET Web API或者是ASP.NET MVC,毕竟是微软官方出产的,用的人也多。 但是呢,NancyFx也是一个很不错的选择。
原文: 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之大杂绘 

 

目录
相关文章
|
15天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
31 4
|
5月前
|
Java API
深入探讨 Java 8 集合操作:全面解析 Stream API 的强大功能
深入探讨 Java 8 集合操作:全面解析 Stream API 的强大功能
72 2
|
22天前
|
机器学习/深度学习 算法 Java
通过 Java Vector API 利用 SIMD 的强大功能
通过 Java Vector API 利用 SIMD 的强大功能
33 10
|
13天前
|
移动开发 前端开发 JavaScript
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
72 0
|
2月前
|
JSON 搜索推荐 API
深入了解亚马逊商品详情API:功能、作用与实例
亚马逊商品详情API接口由官方提供,允许开发者通过程序调用获取商品详细信息,如标题、价格等,适用于电商数据分析、搜索及个性化推荐等场景。接口名称包括ItemLookup、GetMatchingProductForId等,支持HTTP POST/GET请求,需提供商品ID、API密钥及其他可选参数。返回数据格式通常为JSON或XML,涵盖商品详情、分类、品牌、价格、图片URL及用户评价等。该接口对数据收集、实时推荐、营销活动及数据分析至关重要,有助于提升电商平台的数据处理能力、用户体验及商家运营效率。使用时需注册亚马逊开发者账号并申请API访问权限,获取API密钥后按文档构建请求并处理响应数据。
|
3月前
|
存储 JavaScript 前端开发
探索React状态管理:Redux的严格与功能、MobX的简洁与直观、Context API的原生与易用——详细对比及应用案例分析
【8月更文挑战第31天】在React开发中,状态管理对于构建大型应用至关重要。本文将探讨三种主流状态管理方案:Redux、MobX和Context API。Redux采用单一存储模型,提供预测性状态更新;MobX利用装饰器语法,使状态修改更直观;Context API则允许跨组件状态共享,无需第三方库。每种方案各具特色,适用于不同场景,选择合适的工具能让React应用更加高效有序。
66 0
|
3月前
|
JavaScript 网络协议 API
【Azure API 管理】Azure APIM服务集成在内部虚拟网络后,在内部环境中打开APIM门户使用APIs中的TEST功能失败
【Azure API 管理】Azure APIM服务集成在内部虚拟网络后,在内部环境中打开APIM门户使用APIs中的TEST功能失败
|
3月前
|
监控 Cloud Native 容灾
核心系统转型问题之API网关在云原生分布式核心系统中的功能如何解决
核心系统转型问题之API网关在云原生分布式核心系统中的功能如何解决
|
3月前
|
Web App开发 缓存 小程序
【Azure API 管理】从微信小程序访问APIM出现200空响应的问题中发现CORS的属性[terminate-unmatched-request]功能
【Azure API 管理】从微信小程序访问APIM出现200空响应的问题中发现CORS的属性[terminate-unmatched-request]功能
|
6月前
|
存储 缓存 安全
API在Visual Basic中的应用:连接外部服务与扩展功能
【4月更文挑战第27天】本文探讨了在Visual Basic中使用API连接外部服务和扩展功能的方法,涵盖了API的基本概念、种类及如何使用本地和Web API。通过DllImport调用本地API,利用HttpClient和WebClient与Web API交互,同时强调了第三方API的使用和SOA架构中的API角色。安全性、性能优化和错误处理是实践中的关键点。案例研究和最佳实践有助于开发者更有效地利用API,提升Visual Basic应用程序的功能和灵活性。随着API技术的发展,Visual Basic将持续支持开发者创造更强大的应用。
56 0