Nancy之實現API的功能

weixin_34391854發表於2018-05-21
原文: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之大雜繪 

 

相關文章