閱讀253 返回首頁    go 阿裏雲 go 技術社區[雲棲]


通過擴展讓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 IDictionary<string, string> headers)
  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 IDictionary<string, string> GenerateResponseHeaders(HttpRequestMessage request)
  27:     {
  28:         //設置響應報頭"Access-Control-Allow-Methods"
  29:         string origin = request.Headers.GetValues("Origin").First();
  30:         Dictionary<string, string> headers = new Dictionary<string, string>();
  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 Task<HttpResponseMessage> SendAsync(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<CorsAttribute>().FirstOrDefault()??
  23:             controllerDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault();
  24:         if(null == corsAttribute)
  25:         {
  26:             return base.SendAsync(request, cancellationToken);
  27:         }
  28:  
  29:         //利用CorsAttribute實施授權並生成響應報頭
  30:         IDictionary<string,string> headers;
  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<HttpResponseMessage>(response);
  56:     }
  57: }

具體來說,我們通過注冊到當前ServicesContainer上的HttpActionSelector根據請求得到描述目標Action的HttpActionDescriptor對象,為此我們需要根據請求手工生成作為HttpActionSelector的SelectAction方法參數的HttpControllerContext對象。對此有一點需要注意:由於預檢請求采用的HTTP方法為“OPTIONS”,我們需要將其替換成代表真正跨域資源請求的HTTP方法,也就是預檢請求的“Access-Control-Request-Method”報頭值。

在得到描述目標Action的HttpActionDescriptor對象後,我們調用其GetCustomAttributes<T>方法得到應用在Action方法上的CorsAttribute特性。如果這樣的特性不存在,在調用同名方法得到應用在HttpController類型上的CorsAttribute特性。

接下來我們調用CorsAttribute的TryEvaluate方法對請求實施資源授權檢查並得到一組CORS響應報頭,作為參數的HttpRequestMessage對象的HTTP方法應該恢複其原有的值。對於預檢請求,在請求通過授權檢查之後我們會創建一個狀態為“200, OK”的響應,否則會根據錯誤消息創建創建一個狀態為“400, Bad Request”的響應。

對於非預檢請求來說(可能是簡單跨域資源請求,也可能是繼預檢請求之後發送的真正的跨域資源請求),我們調用基類的SendAsync方法將請求交付給後續的HttpMessageHandler進行處理並最終得到最終的響應。我們最終將調用CorsAttribute的TryEvaluate方法得到的響應報頭逐一添加到響應報頭列表中。

四、CorsMessageHandler針對簡單跨域資源請求的授權檢驗

09090039-9bc3d214334a43efbda28a778255533
09090040-53a7f1f213fa442a815a08d2bc2f255

接下來我們通過於一個簡單的實例來演示同源策略針對跨域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<IEnumerable<Contact>>對象,但是該方法上麵應用了我們定義的CorsAttribute特性,並將“https://localhost:9527”(客戶端ASP.NET MVC應用的站點)設置為允許授權的站點。

   1: public class ContactsController : ApiController
   2: {
   3:     [Cors("https://localhost:9527")] 
   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<IEnumerable<Contact>>(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: <html>
   2: <head>
   3:     <title>聯係人列表</title>
   4: <script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>
   1:  
   2: </head>
   3: <body>
   4:     <ul id="contacts"></ul>
   5:     <script type="text/javascript">
   6:         $(function ()
   7:         {
   8:             var url = "https://localhost:3721/api/contacts";
   9:             $.getJSON(url, null, function (contacts) {
  10:                 $.each(contacts, function (index, contact)
  11:                 {
  12:                     var html = "<li><ul>";
  13:                     html += "<li>Name: " + contact.Name + "</li>";
  14:                     html += "<li>Phone No:" + contact.PhoneNo + "</li>";
  15:                     html += "<li>Email Address: " + contact.EmailAddress + "</li>";
  16:                     html += "</ul>";
  17:                     $("#contacts").append($(html));
  18:                 });
  19:             });
  20:         });
  21:     
</script>
   5: </body>
   6: </html>

如果我們利用Fiddler來檢測針對Web API調用的Ajax請求,如下所示的請求和響應內容會被捕捉到,我們可以清楚地看到利用CorsMessageHandler添加的“Access-Control-Allow-Origin”報頭出現在響應的報頭集合中。

   1: GET https://localhost:3721/api/contacts HTTP/1.1
   2: Host: localhost:3721
   3: Connection: keep-alive
   4: Accept: application/json, text/javascript, */*; q=0.01
   5: Origin: https://localhost:9527
   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: https://localhost:9527/
   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: https://localhost:9527 
  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: <html>
   2: <head>
   3:     <title>聯係人列表</title>
   4: <script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>
   1:  

   2: </head>

   3: <body>

   4:     <ul id="contacts"></ul>

   5:     <script type="text/javascript">

   6:         $(function ()

   7:         {

   8:             $.ajax({

   9:                 url         : 'https://localhost:3721/api/contacts',

  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 = "<li><ul>";

  20:                 html += "<li>Name: " + contact.Name + "</li>";

  21:                 html += "<li>Phone No:" + contact.PhoneNo + "</li>";

  22:                 html += "<li>Email Address: " + contact.EmailAddress + "</li>";

  23:                 html += "</ul>";

  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:     
 </script>
   5: </body>
   6: </html>

再次運行我們的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 https://localhost:3721/api/contacts 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: https://localhost:9527
   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: https://localhost:9527/
  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: https://localhost:9527 
  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 https://localhost:3721/api/contacts HTTP/1.1
  30: Host: localhost:3721
  31: Connection: keep-alive
  32: Accept: */*
  33: X-Custom-Header1: Foo
  34: Origin: https://localhost:9527
  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: https://localhost:9527/
  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: https://localhost:9527
  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}]

 

CORS係列文章
[1] 同源策略與JSONP
[2] 利用擴展讓ASP.NET Web API支持JSONP
[3] W3C的CORS規範
[4] 利用擴展讓ASP.NET Web API支持CORS
[5] ASP.NET Web API自身對CORS的支持: 從實例開始
[6] ASP.NET Web API自身對CORS的支持: CORS授權策略的定義和提供
[7] ASP.NET Web API自身對CORS的支持: CORS授權檢驗的實施
[8] ASP.NET Web API自身對CORS的支持: CorsMessageHandler

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 15:33:44

  上一篇:go  通過擴展讓ASP.NET Web API支持JSONP
  下一篇:go  ASP.NET Web API自身對CORS的支持:從實例開始