透過擴充套件讓ASP.NET Web API支援W3C的CORS規範
讓ASP.NET Web API支援JSONP和W3C的CORS規範是解決“跨域資源共享”的兩種途徑,在《透過擴充套件讓ASP.NET Web API支援JSONP》中我們實現了前者,並且在《W3C的CORS Specification》一文中我們對W3C的CORS規範進行了詳細介紹,現在我們透過一個具體的例項來演示如何利用ASP.NET Web API具有的擴充套件點來實現針對CORS的支援。
目錄
一、ActionFilter OR HttpMessageHandler
二、用於定義CORS資源授權策略的特性——CorsAttribute
三、實施CORS授權檢驗的HttpMessageHandler——CorsMessageHandler
四、CorsMessageHandler針對簡單跨域資源請求的授權檢驗
五、CorsMessageHandler針對Preflight Request的授權檢驗
一、ActionFilter OR HttpMessageHandler
透過上面針對W3C的CORS規範的介紹,我們知道跨域資源共享實現的途徑就是資源的提供者利用預定義的響應報頭表明自己是否將提供的資源授權給了客戶端JavaScript程式,而支援CORS的瀏覽器利用這些響應報頭決定是否允許JavaScript程式操作返回的資源。對於ASP .NET Web API來說,如果我們具有一種機制能夠根據預定義的資源授權規則自動生成和新增針對CORS的響應報頭,那麼資源的跨域共享就迎刃而解了。
那麼如何利用ASP.NET Web API的擴充套件實現針對CORS響應報頭的自動新增呢?可能有人首先想到的是利用HttpActionFilter在目標Action方法執行之後自動新增CORS響應報頭。這種解決方案對於簡單跨域資源請求是沒有問題的,但是不要忘了:對於非簡單跨域資源請求,瀏覽器會採用“預檢(Preflight)”機制。目標Action方法只會在處理真正跨域資源請求的過程中才會執行,但是對於採用“OPTIONS”作為HTTP方法的預檢請求,根本找不到匹配的目標Action方法。
為了能夠有效地應付瀏覽器採用的預檢機制,我們只能在ASP.NET Web API的訊息處理管道級別實現對提供資源的授權檢驗和對CORS響應報頭的新增。我們只需要為此建立一個自定義的HttpMessageHandler即可,不過在此之前我們先來介紹用於定義資源授權策略的CorsAttribute特性。
二、用於定義CORS資源授權策略的特性——CorsAttribute
我們將具有如下定義的CorsAttribute特性直接應用到某個HttpController或者定義其中的某個Action方法上來定義相關的資源授權策略。簡單起見,我們的授權策略只考慮請求站點,而忽略請求提供的自定義報頭和攜帶的使用者憑證。如下面的程式碼片斷所示,CorsAttribute具有一個只讀屬性AllowOrigins表示一組被授權站點對應的Uri陣列,具體站點列表在建構函式中指定。另一個只讀屬性ErrorMessage表示在請求沒有透過授權檢驗情況下返回的錯誤訊息。
1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method)]
2: public class CorsAttribute: Attribute
3: {
4: public Uri[] AllowOrigins { get; private set; }
5: public string ErrorMessage { get; private set; }
6:
7: public CorsAttribute(params string[] allowOrigins)
8: {
9: this.AllowOrigins = (allowOrigins ?? new string[0]).Select(origin => new Uri(origin)).ToArray();
10: }
11:
12: public bool TryEvaluate(HttpRequestMessage request, out IDictionaryheaders)
13: {
14: headers = null;
15: string origin = request.Headers.GetValues("Origin").First();
16: Uri originUri = new Uri(origin);
17: if (this.AllowOrigins.Contains(originUri))
18: {
19: headers = this.GenerateResponseHeaders(request);
20: return true;
21: }
22: this.ErrorMessage = "Cross-origin request denied";
23: return false;
24: }
25:
26: private IDictionaryGenerateResponseHeaders(HttpRequestMessage request)
27: {
28: //設定響應報頭"Access-Control-Allow-Methods"
29: string origin = request.Headers.GetValues("Origin").First();
30: Dictionaryheaders = new Dictionary ();
31: headers.Add("Access-Control-Allow-Origin", origin);
32: if (request.IsPreflightRequest())
33: {
34: //設定響應報頭"Access-Control-Request-Headers"
35: //和"Access-Control-Allow-Headers"
36: headers.Add("Access-Control-Allow-Methods", "*");
37: string requestHeaders = request.Headers.GetValues("Access-Control-Request-Headers").FirstOrDefault();
38: if (!string.IsNullOrEmpty(requestHeaders))
39: {
40: headers.Add("Access-Control-Allow-Headers", requestHeaders);
41: }
42: }
43: return headers;
44: }
45: }
我們將針對請求的資源授權檢查定義在TryEvaluate方法中,其返回至表示請求是否透過了授權檢查,輸出引數headers透過返回的字典物件表示最終新增的CORS響應報頭。在該方法中,我們從指定的HttpRequestMessage物件中提取表示請求站點的“Origin”報頭值。如果請求站點沒有在透過AllowOrigins屬性表示的授權站點內,則意味著請求沒有透過授權檢查,在此情況下我們會將ErrorMessage屬性設定為“Cross-origin request denied”。
在請求成功透過授權檢查的情況下,我們呼叫另一個方法GenerateResponseHeaders根據請求生成我們需要的CORS響應報頭。如果當前為簡單跨域資源請求,只會返回針對“Access-Control-Allow-Origin”的響應報頭,其值為請求站點。對於預檢請求來說,我們還需要額外新增針對“Access-Control-Request-Headers”和“Access-Control-Allow-Methods”的響應報頭。對於前者,我們直接採用請求的“Access-Control-Request-Headers”報頭值,而後者被直接設定為“*”。
在上面的程式中,我們透過呼叫HttpRequestMessage的擴充套件方法IsPreflightRequest來判斷是否是一個預檢請求,該方法定義如下。從給出的程式碼片斷可以看出,我們判斷預檢請求的條件是:包含報頭“Origin”和“Access-Control-Request-Method”的HTTP-OPTIONS請求。
1: public static class HttpRequestMessageExtensions
2: {
3: public static bool IsPreflightRequest(this HttpRequestMessage request)
4: {
5: return request.Method == HttpMethod.Options &&
6: request.Headers.GetValues("Origin").Any() &&
7: request.Headers.GetValues("Access-Control-Request-Method").Any();
8: }
9: }
三、實施CORS授權檢驗的HttpMessageHandler——CorsMessageHandler
針對跨域資源共享的實現最終體現在具有如下定義的CorsMessageHandler型別上,它直接繼承自DelegatingHandler。在實現的SendAsync方法中,CorsMessageHandler利用應用在目標Action方法或者HttpController型別上CorsAttribute來對請求實施授權檢驗,最終將生成的CORS報頭新增到響應報頭列表中。
1: public class CorsMessageHandler: DelegatingHandler
2: {
3: protected override TaskSendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
4: {
5: //得到描述目標Action的HttpActionDescriptor
6: HttpMethod originalMethod = request.Method;
7: bool isPreflightRequest = request.IsPreflightRequest();
8: if (isPreflightRequest)
9: {
10: string method = request.Headers.GetValues("Access-Control-Request-Method").First();
11: request.Method = new HttpMethod(method);
12: }
13: HttpConfiguration configuration = request.GetConfiguration();
14: HttpControllerDescriptor controllerDescriptor = configuration.Services.GetHttpControllerSelector().SelectController(request);
15: HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), request.GetRouteData(), request)
16: {
17: ControllerDescriptor = controllerDescriptor
18: };
19: HttpActionDescriptor actionDescriptor = configuration.Services.GetActionSelector().SelectAction(controllerContext);
20:
21: //根據HttpActionDescriptor得到應用的CorsAttribute特性
22: CorsAttribute corsAttribute = actionDescriptor.GetCustomAttributes().FirstOrDefault()??
23: controllerDescriptor.GetCustomAttributes().FirstOrDefault();
24: if(null == corsAttribute)
25: {
26: return base.SendAsync(request, cancellationToken);
27: }
28:
29: //利用CorsAttribute實施授權並生成響應報頭
30: IDictionaryheaders;
31: request.Method = originalMethod;
32: bool authorized = corsAttribute.TryEvaluate(request, out headers);
33: HttpResponseMessage response;
34: if (isPreflightRequest)
35: {
36: if (authorized)
37: {
38: response = new HttpResponseMessage(HttpStatusCode.OK);
39: }
40: else
41: {
42: response = request.CreateErrorResponse(HttpStatusCode.BadRequest, corsAttribute.ErrorMessage);
43: }
44: }
45: else
46: {
47: response = base.SendAsync(request, cancellationToken).Result;
48: }
49:
50: //新增響應報頭
51: foreach (var item in headers)
52: {
53: response.Headers.Add(item.Key, item.Value);
54: }
55: return Task.FromResult(response);
56: }
57: }
具體來說,我們透過註冊到當前ServicesContainer上的HttpActionSelector根據請求得到描述目標Action的HttpActionDescriptor物件,為此我們需要根據請求手工生成作為HttpActionSelector的SelectAction方法引數的HttpControllerContext物件。對此有一點需要注意:由於預檢請求採用的HTTP方法為“OPTIONS”,我們需要將其替換成代表真正跨域資源請求的HTTP方法,也就是預檢請求的“Access-Control-Request-Method”報頭值。
在得到描述目標Action的HttpActionDescriptor物件後,我們呼叫其GetCustomAttributes
接下來我們呼叫CorsAttribute的TryEvaluate方法對請求實施資源授權檢查並得到一組CORS響應報頭,作為引數的HttpRequestMessage物件的HTTP方法應該恢復其原有的值。對於預檢請求,在請求透過授權檢查之後我們會建立一個狀態為“200, OK”的響應,否則會根據錯誤訊息建立建立一個狀態為“400, Bad Request”的響應。
對於非預檢請求來說(可能是簡單跨域資源請求,也可能是繼預檢請求之後傳送的真正的跨域資源請求),我們呼叫基類的SendAsync方法將請求交付給後續的HttpMessageHandler進行處理並最終得到最終的響應。我們最終將呼叫CorsAttribute的TryEvaluate方法得到的響應報頭逐一新增到響應報頭列表中。
四、CorsMessageHandler針對簡單跨域資源請求的授權檢驗
接下來我們透過於一個簡單的例項來演示同源策略針對跨域Ajax請求的限制。如圖右圖所示,我們利用Visual Studio在同一個解決方案中建立了兩個Web應用。從專案名稱可以看出,WebApi和MvcApp分別為ASP.NET Web API和MVC應用,後者是Web API的呼叫者。我們直接採用預設的IIS Express作為兩個應用的宿主,並且固定了埠號:WebApi和MvcApp的埠號分別為“3721”和“9527”,所以指向兩個應用的URI肯定不可能是同源的。我們在WebApi應用中定義瞭如下一個繼承自ApiController的ContactsController型別,它具有的唯一Action方法GetAllContacts返回一組聯絡人列表。
如下面的程式碼片斷所示,用於獲取所有聯絡人列表的Action方法GetAllContacts返回一個JsonResult
1: public class ContactsController : ApiController
2: {
3: [Cors("")]
4: public IHttpActionResult GetAllContacts()
5: {
6: Contact[] contacts = new Contact[]
7: {
8: new Contact{ Name="張三", PhoneNo="123", EmailAddress="zhangsan@gmail.com"},
9: new Contact{ Name="李四", PhoneNo="456", EmailAddress="lisi@gmail.com"},
10: new Contact{ Name="王五", PhoneNo="789", EmailAddress="wangwu@gmail.com"},
11: };
12: return Json>(contacts);
13: }
14: }
在Global.asax中,我們採用如下的方式將一個CorsMessageHandler物件新增到ASP.NET Web API的訊息處理管道中。
1: public class WebApiApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsMessageHandler ());
6: //其他操作
7: }
8: }
接下來們在MvcApp應用中定義如下一個HomeController,預設的Action方法Index會將對應的View呈現出來。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: return View();
6: }
7: }
如下所示的是Action方法Index對應View的定義。我們的目的在於:當頁面成功載入之後以Ajax請求的形式呼叫上面定義的Web API獲取聯絡人列表,並將自呈現在頁面上。如下面的程式碼片斷所示,Ajax呼叫和返回資料的呈現是透過呼叫jQuery的getJSON方法完成的。在此基礎上直接呼叫我們的ASP.NET MVC程式照樣會得到如右圖所示的結果.
1:
2:
3:聯絡人列表
4: 1: 2: 3: 4:
5:
6:
如果我們利用Fiddler來檢測針對Web API呼叫的Ajax請求,如下所示的請求和響應內容會被捕捉到,我們可以清楚地看到利用CorsMessageHandler新增的“Access-Control-Allow-Origin”報頭出現在響應的報頭集合中。
1: GET HTTP/1.1
2: Host: localhost:3721
3: Connection: keep-alive
4: Accept: application/json, text/javascript, */*; q=0.01
5: Origin:
6: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
7: Referer: /
8: Accept-Encoding: gzip,deflate,sdch
9: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
10:
11: HTTP/1.1 200 OK
12: Cache-Control: no-cache
13: Pragma: no-cache
14: Content-Length: 205
15: Content-Type: application/json; charset=utf-8
16: Expires: -1
17: Server: Microsoft-IIS/8.0
18: Access-Control-Allow-Origin:
19: X-AspNet-Version: 4.0.30319
20: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPnmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAzXFdlYkFwaVxhcGlcY29udGFjdHM=?=
21: X-Powered-By: ASP.NET
22: Date: Wed, 04 Dec 2013 01:50:01 GMT
23:
24: [{"Name":"張三","PhoneNo":"123","EmailAddress":"zhangsan@gmail.com"},{"Name":"李四","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"王五","PhoneNo":"789","EmailAddress":wangwu@gmail.com}]
五、CorsMessageHandler針對Preflight Request的授權檢驗
從上面給出的請求和響應內容可以確定Web API的呼叫採用的是“簡單跨域資源請求”,所以並沒有採用“預檢”機制。如何需要迫使瀏覽器採用預檢機制,就需要了解我們在《W3C的CORS Specification》上面提到的簡單跨域資源請求具有的兩個條件
採用簡單HTTP方法(GET、HEAD和POST);
不具有非簡單請求報頭的自定義報頭。
只要打破其中任何一個條件就會迫使瀏覽器採用預檢機制,我們選擇為請求新增額外的自定義報頭。在ASP.NET MVC應用使用者呼叫Web API的View中,針對Ajax請求呼叫Web API的JavaScript程式被改寫成如下的形式:我們在傳送Ajax請求之前利用setRequestHeader函式新增了兩個名稱分別為“'X-Custom-Header1”和“'X-Custom-Header2”的自定義報頭。
1:
2:
3:聯絡人列表
4:
1:
2:
3:
4:
5:
6: $(function ()
7: {
8: $.ajax({
9: url : '',
10: type : 'GET',
11: success : listContacts,
12: beforeSend : setRequestHeader
13: });
14: });
15:
16: function listContacts(contacts)
17: {
18: $.each(contacts, function (index, contact) {
19: var html = "
- ";
20: html += "
21: html += "
22: html += "
23: html += "";
24: $("#contacts").append($(html));
25: });
26: }
27:
28: function setRequestHeader(xmlHttpRequest)
29: {
30: xmlHttpRequest.setRequestHeader('X-Custom-Header1', 'Foo');
31: xmlHttpRequest.setRequestHeader('X-Custom-Header2', 'Bar');
32: }
33:
5:
6:
再次執行我們的ASP.NET MVC程式,依然會得正確的輸出結果,但是針對Web API的呼叫則會涉及到兩次訊息交換,分別針對預檢請求和真正的跨域資源請求。從下面給出的兩次訊息交換涉及到的請求和響應內容可以看出:自定義的兩個報頭名稱會出現在採用“OPTIONS”作為HTTP方法的預檢請求的“Access-Control-Request-Headers”報頭中,利用CorsMessageHandler新增的3個報頭(“Access-Control-Allow-Origin”、“Access-Control-Allow-Methods”和“Access-Control-Allow-Headers”)均出現在針對預檢請求的響應中。
1: OPTIONS HTTP/1.1
2: Host: localhost:3721
3: Connection: keep-alive
4: Cache-Control: max-age=0
5: Access-Control-Request-Method: GET
6: Origin:
7: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
8: Access-Control-Request-Headers: accept, x-custom-header1, x-custom-header2
9: Accept: */*
10: Referer: /
11: Accept-Encoding: gzip,deflate,sdch
12: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
13:
14: HTTP/1.1 200 OK
15: Cache-Control: no-cache
16: Pragma: no-cache
17: Expires: -1
18: Server: Microsoft-IIS/8.0
19: Access-Control-Allow-Origin:
20: Access-Control-Allow-Methods: *
21: Access-Control-Allow-Headers: accept, x-custom-header1, x-custom-header2
22: X-AspNet-Version: 4.0.30319
23: X-SourceFiles: =?UTF-8?B??=
24: X-Powered-By: ASP.NET
25: Date: Wed, 04 Dec 2013 02:11:16 GMT
26: Content-Length: 0
27:
28: --------------------------------------------------------------------------------
29: GET HTTP/1.1
30: Host: localhost:3721
31: Connection: keep-alive
32: Accept: */*
33: X-Custom-Header1: Foo
34: Origin:
35: X-Custom-Header2: Bar
36: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
37: Referer: /
38: Accept-Encoding: gzip,deflate,sdch
39: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
40:
41: HTTP/1.1 200 OK
42: Cache-Control: no-cache
43: Pragma: no-cache
44: Content-Length: 205
45: Content-Type: application/json; charset=utf-8
46: Expires: -1
47: Server: Microsoft-IIS/8.0
48: Access-Control-Allow-Origin:
49: X-AspNet-Version: 4.0.30319
50: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XF9udGFjdHM=?=
51: X-Powered-By: ASP.NET
52: Date: Wed, 04 Dec 2013 02:11:16 GMT
53:
54: [{"Name":"張三","PhoneNo":"123","EmailAddress":"zhangsan@gmail.com"},{"Name":"李四","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"王五","PhoneNo":"789","EmailAddress":wangwu@gmail.com}]
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2480/viewspace-2805181/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- ASP.NET Web API自身對CORS的支援:從例項開始ASP.NETWebAPICORS
- Java通過URLClassLoader讓程式支援外掛擴充套件(轉載)Java套件
- 如何擴充套件大規模Web網站的效能?套件Web網站
- ASP.NET Core擴充套件庫之Http通用擴充套件ASP.NET套件HTTP
- 表空間自動擴充套件 AUTOALLOCATE 的擴充套件規律套件
- C 擴充套件庫 – mysql API套件MySqlAPI
- W3C付款請求API:Payment-Request API呼叫規範API
- Web儲存(Web Storage)擴充套件EStorageWeb套件
- [擴充套件分享] 多多客 API SDK套件API
- 【擴充套件分享】多多客 API SDK套件API
- 如何擴充套件Kubernetes API?套件API
- 介紹一個能避免 CORS 錯誤的 Chrome 擴充套件 - Moesif Origin & CORS ChangerCORSChrome套件
- 18個擴充套件讓你的Firefox成為滲透測試工具套件Firefox
- 讓ASP.NET Web API的Action方法ASP.NETWebAPI
- (16) SpringCloud-Eureka的REST API及API擴充套件SpringGCCloudRESTAPI套件
- 在 easywechat 包上擴充套件 API 介面套件API
- ASP.NET Core擴充套件庫之日誌ASP.NET套件
- ASP.NET 頁面aspx之擴充套件ASP.NET套件
- kotlin 擴充套件(擴充套件函式和擴充套件屬性)Kotlin套件函式
- 透過開發VB外掛來擴充套件VB應用 (轉)套件
- 高效擴充套件工具讓 VS Code 如虎添翼套件
- 【外掛】cors:vscode cors 擴充套件 - 解決跨域開發最終版CORSVSCode套件跨域
- Atitit.一個cms有多少少擴充套件點,多少api wordpress cms有多少api。。擴充套件點...套件API
- 重構 - 設計API的擴充套件機制API套件
- Lyft如何透過DevOps提升擴充套件微服務的生產力? - Garrettdev套件微服務
- 【重典】Asp.net MVC Fckeditor的擴充套件(支援PV3及自動繫結)ASP.NETMVC套件
- 平穩擴充套件:可支援RevenueCat每日12億次API請求的快取套件API快取
- 簡訊:W3C 釋出 CSS 選擇器 API 規範CSSAPI
- Monitor的擴充套件支援string的超時鎖套件
- Web API對application/json內容型別的CORS支援WebAPIAPPJSON型別CORS
- 使用aggregation API擴充套件你的kubernetes APIAPI套件
- Chrome瀏覽器擴充套件開發系列之十七:擴充套件中可用的chrome.events APIChrome瀏覽器套件API
- 虛擬主機支援哪些擴充套件功能套件
- ASP.NET擴充套件庫之Http日誌ASP.NET套件HTTP
- 詳解ASP.NET頁面的aspx擴充套件ASP.NET套件
- HTML5新增API之DOM 擴充套件HTMLAPI套件
- 【鄒健】Asp.net MVC Fckeditor的擴充套件(支援PV3及自動繫結)ASP.NETMVC套件
- WCF擴充套件:行為擴充套件Behavior Extension套件