Host ASP.NET WebApi in Owin
什麽是OWIN
Owin其實是微軟為了解耦.Net Web app對IIS的依賴而制定的一套規範,規範定義了Web Server與Web App之間的接口,這樣Web App就可以Host在所有兼容OWIN規範的Web Server了(包含控制臺應用和Windows服務...)。具體來說,Owin將Web app和Web Server整體劃分為以下幾個模塊。
Host
根據官方解釋,Host是指server和application所依托執行的進程。主要負責應用的啟動與配置。
Server
Server負責直接與客戶端進行Http通信,然後將請求轉換為Owin的語義,然後用Owin的流程進行處理的Http Server.
Middleware
Middleware是開發人員自行註冊到Owin處理請求管道中的模塊(類似IIS中的Module),可以直接參與請求的處理。所以我認為Web Api框架其實也屬於Middleware.(不過官方將Web Api框架歸屬於Web Framework。它會將Owin的語義轉換為Web Framework內部的語義,然後按照內部的處理流程處理請求)
Application
Application就是基於所有上面所有Middleware(準確說應該是Web Framework,看來官方定義一個Web Framework模塊是有意義的)所構建的應用層。
這些定義只是一些描述,那具體實施是怎麽實施呢。微軟自己有一個開源項目叫Katana,它是對Owin規範的官方實現(其實主要就是實現上述的Host/Server部分,因為Middleware,Application部分都是需要我們自己開發的)。下面通過使用Katana,我們將web api部署在一個控制臺進程中,來看看具體怎麽去使用它以及OWIN接口到底是什麽。
Host WebApi in Console App
1.首先我們創建一個控制臺應用
2.然後我們通過Nuget引入Package Microsoft.AspNet.WebApi.OwinSelfHost,安裝過程中所有依賴的Owin和Web Api的package都會一並安裝。
3.我們可以添加一個ApiController:PersonController,並添加一個接口方法:
1 [RoutePrefix("api/persons")] 2 public class PersonController : ApiController 3 { 4 [Route("{id}/name")] 6 public string getName(string id) 7 { 8 return id + "@boss"; 9 } 10 }
4. 接下來我們需要將web api進行配置和部署。該Katana登場了。
首先按照Owin約定我們得添加一個用於Startup的類,這個類中需要有一個簽名為Configuration(IAppBuilder app)的方法:
public class Startup { public void Configuration(IAppBuilder appBuilder) {var configuration = new HttpConfiguration(); configuration.MapHttpAttributeRoutes();//配置web api的router appBuilder.UseWebApi(configuration);//這個擴展是由package Microsoft.AspNet.WebApi.Owin提供,它負責註冊web api到owin的處理管道中,
//並在處理請求時將Owin語義與web api中的語義進行轉換 } }
我們在Main函數中添加如下代碼:
WebApp.Start<Startup>("http://localhost:8088/");//WebApp利用katana實現的OwinHttpListener來在指定url上監聽http請求。 Console.WriteLine("Started!"); Console.ReadKey();
這樣我們就基於katana實現了在console app中運行web api了。
F5運行以下,看看效果。
然後可以通過瀏覽器訪問http://localhost:8088/api/persons/123/name,應該能看到如下畫面
Add Authenticate Middleware
接下來我們通過添加owin middleware的方式來為web api添加保護機制(Authentication)。在這之前我先解釋下關於Middleware的基礎知識。
Middleware是一組Owin server在處理http請求的時候會輪流調用到的模塊,他們通過調用IAppBuilder的Use擴展方法來註冊。運行時的Middleware的調用順序與註冊順序一致。
並且對管道中下一Middleware的調用是由當前執行的Middleware來執行。具體到接口來說是這樣:
1. OWIN定義了一個Middleware的執行接口Func<IDictionary<string,object>,Task>,然後要求每個Middleware的定義需滿足如下條件:
- 提供接受一個類型為執行接口類型(Func<IDictionary<string,object>,Task>)的構造函數
- 提供一個滿足如下簽名的方法Task Invoke(IDictionary<string,object> parameters)
也就是說,owin將每個middleware最後都抽象成了一個函數,這個函數接受IDictionary作為參數,返回一個執行具體處理的Task.
2. OWIN在創建每個Middleware的實例時,會根據註冊順序傳入當前Middleware在執行管道中的下一個Middleware的執行接口。Middleware需要存儲起來後續調用。
3. 當處理請求時,Server會調用管道中第一個Middleware的Invoke方法,然後由該Middleware決定處理完請求後是否調用下一個Middleware. 在調用Invoke時,Owin server
會將當前請求的所有上下文屬性傳入該Dictionary對象中。詳細的上下文屬性列表見官方文檔。
我們在當前項目中新建AuthenticateMiddleWare,代碼如下:
public class AuthenticateMiddleware { private Func<IDictionary<string, object>, Task> nextAppFunc; public AuthenticateMiddleware(Func<IDictionary<string, object>, Task> nextMiddleWareFunc) { nextAppFunc = nextMiddleWareFunc; } public async Task Invoke(IDictionary<string, object> parameters) { Console.WriteLine("Authenticating"); string queryString = parameters["owin.RequestQueryString"] as string;//獲取http請求的query string var respStream = parameters["owin.ResponseBody"] as Stream;//獲取http請求的response stream var streamWriter = new StreamWriter(respStream); var queryDic = ParseQueryString(queryString); const string tokenKey = "token"; const string predefineToken = "88888888"; if (!queryDic.ContainsKey(tokenKey)||queryDic[tokenKey]!=predefineToken)//檢查請求中所帶token是否合法,此處僅為測試需要,直接硬編碼。 {
//如果token非法,則直接寫入Access Denied到response中。停止繼續執行管道中其他middleware. streamWriter.WriteLine("Access Denied!"); streamWriter.Flush(); return; }
var identity = new GenericIdentity("boss zhang"); parameters["server.User"] = new GenericPrincipal(identity, new string[] { "admin" });//token合法,生成principal對象到parameters中,key "server.User"
//用於存儲當前請求的user信息,相當於HttpContext.User if (nextAppFunc != null) { await nextAppFunc.Invoke(parameters);//繼續執行管道中下一個middleware } } private Dictionary<string, string> ParseQueryString(string originalString) { string[] queryStringItems = originalString.Split(new string[] { "&" }, StringSplitOptions.RemoveEmptyEntries); var queryStringDic = new Dictionary<string, string>(); foreach (var item in queryStringItems) { string[] queryStringKvp = item.Split(new string[] { "=" }, StringSplitOptions.None); if (queryStringKvp.Length == 2) { queryStringDic[queryStringKvp[0]] = queryStringKvp[1]; } } return queryStringDic; }
}
然後再給之前定義的PersonController加上Authorize Attribute加以保護。
F5運行,然後在瀏覽器中訪問以下url: http://localhost:8088/api/persons/123/name?token=88888888.依然能得到之前正確的返回。
如果去掉token=88888888,則得到如下結果。
這說明我們的AuthenticateMiddle發揮作用了。
完整代碼見https://github.com/lbwxly/OwinSample.git
參考文檔:
http://www.dotnetcurry.com/signalr/915/owin-katana-signalr-web-server
https://ovaismehboob.com/2014/12/01/understanding-owin-by-developing-a-custom-owin-middleware-component/
Host ASP.NET WebApi in Owin