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公司發出請求帶的參數有:
通過這些參數,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 }
拼接起來,並加上私鑰來加密。這里為了偷懶,私鑰直接在代碼里了寫死了。正常情況下應該將私鑰存放在數據庫中的,有一個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的寫法也在注釋中有提到。
到這里,這個簡單的接口已經能夠正常運行了,下面來看看效果吧:
正確無誤的訪問鏈接如下:
我們修改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之大雜繪

