透過擴充套件讓ASP.NET Web API支援W3C的CORS規範

wh7577發表於2021-09-09

讓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 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 GenerateResponseHeaders(HttpRequestMessage request)

   

  27:     {

   

  28:         //設定響應報頭"Access-Control-Allow-Methods"

   

  29:         string origin = request.Headers.GetValues("Origin").First();

   

  30:         Dictionary headers = 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 Task 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().FirstOrDefault()??

   

  23:             controllerDescriptor.GetCustomAttributes().FirstOrDefault();

   

  24:         if(null == corsAttribute)

   

  25:         {

   

  26:             return base.SendAsync(request, cancellationToken);

   

  27:         }

   

  28:

   

  29:         //利用CorsAttribute實施授權並生成響應報頭

   

  30:         IDictionary 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(response);

   

  56:     }

   

  57: }

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

在得到描述目標Action的HttpActionDescriptor物件後,我們呼叫其GetCustomAttributes方法得到應用在Action方法上的CorsAttribute特性。如果這樣的特性不存在,在呼叫同名方法得到應用在HttpController型別上的CorsAttribute特性。

接下來我們呼叫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>物件,但是該方法上面應用了我們定義的CorsAttribute特性,並將“ MVC應用的站點)設定為允許授權的站點。

   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:     

       

       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 += "
    • Name: " + contact.Name + "
    • ";

         


         

        21:                 html += "
    • Phone No:" + contact.PhoneNo + "
    • ";

         


         

        22:                 html += "
    • Email Address: " + contact.EmailAddress + "
    • ";

         


         

        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/,如需轉載,請註明出處,否則將追究法律責任。

      相關文章