跨域資源共享(CORS)

wjaning發表於2021-09-09

在《透過擴充套件讓ASP.NET Web API支援W3C的CORS規範》中,我們透過自定義的HttpMessageHandler自行為ASP.NET Web API實現了針對CORS的支援,實際上ASP.NET Web API自身也是這麼做的,該自定義HttpMessageHandler就是System.Web.Http.Cors.CorsMessageHandler。

   1: public class CorsMessageHandler : DelegatingHandler

   

   2: {

   

   3:     public CorsMessageHandler(HttpConfiguration httpConfiguration);

   

   4:     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);

   

   5:

   

   6:     public virtual Task<HttpResponseMessage> HandleCorsPreflightRequestAsync(HttpRequestMessage request, CorsRequestContext corsRequestContext, CancellationToken cancellationToken);

   

   7:     public virtual Task<HttpResponseMessage> HandleCorsRequestAsync(HttpRequestMessage request, CorsRequestContext corsRequestContext, CancellationToken cancellationToken);

   

   8: }

CorsMessageHandler的核心功能在於:提取預定義的CORS授權策略並對當前請求實施授權檢驗,並根據授權檢驗的結果為現有的響應(針對簡單跨域資源請求和繼預檢請求之後傳送的真正跨域資源請求)或者新建立的響應(針對預檢請求)新增相應的CORS報頭。如上面的程式碼片斷所示,CorsMessageHandler定義了HandleCorsPreflightRequestAsync和HandleCorsRequestAsync虛方法,它們分別實現針對預檢請求和非預檢請求的CORS授權檢驗。

在實現的SendAsync方法中,當CorsRequestContext根據表示當前請求的HttpRequestMessage物件建立之後,會根據其IsPreflight屬性選擇呼叫方法HandleCorsPreflightRequestAsync或者HandleCorsRequestAsync。

CORS授權檢驗

圖片描述

實現在CorsMessageHandler中的具體CORS授權檢驗流程基本上體現在右圖中。它首先根據表示當前請求的HttpRequestMessage物件建立CorsRequestContext物件。然後利用註冊的CorsProviderFactory得到對應的CorsProvider物件,並利用後者得到針對當前請求的資源授權策略,這是一個CorsPolicy物件。

接下來,CorsMessageHandler會獲取註冊的CorsEngine。此前得到的CorsRequestContext和CorsPolicy物件會作為引數呼叫CorsEngine的EvaluatePolicy方法,CORS資源授權檢驗由此開始。授權檢驗結束之後,CorsMessageHandler會得到表示檢驗結果的CorsResult物件。

對於預檢請求,CorsMessageHandler會直接建立HttpResponseMessage物件予以響應。具體來說,如果預檢請求透過了授權檢驗,一個狀態為“200, OK”的HttpResponseMessage會被建立出來,透過CorsResult得到CORS響應報頭會被新增到這個HttpResponseMessage物件的報頭集合中。如果授權檢驗失敗,建立的HttpResponseMessage具有的狀態為“400, Bad Request”,CorsResult攜帶的錯誤響應會作為響應的主體內容。

對於非預檢請求,它會將當前請求傳遞給訊息處理管道的後續部分進行進一步處理,並最終得到表示響應訊息的HttpResponseMessage。只有在請求透過授權檢查的情況下,由CorsResult得到的CORS響應報頭才會被新增到此HttpResponseMessage的報頭集合中。

例項演示:建立MyCorsMessageHandler模擬具體採用的授權檢驗

為了讓讀者朋友們對實現在CorsMessageHandler中的具體CORS資源授權流程具有更加深刻的認識,我們現在將這樣的授權檢驗邏輯實現在一個自定義的HttpMessageHandler中。為此我們定義瞭如下一個MyCorsMessageHandler型別,由於它僅僅用於模擬CorsMessageHandler大體實現邏輯,所以我們會忽略很多細節上(比如異常處理)的程式碼。

   1: public class MyCorsMessageHandler: DelegatingHandler

   

   2: {

   

   3:     protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

   

   4:     {

   

   5:         //根據當前請求建立CorsRequestContext

   

   6:         CorsRequestContext context = request.CreateCorsRequestContext();

   

   7:

   

   8:         //針對非預檢請求:將請求傳遞給訊息處理管道後續部分繼續處理,並得到響應

   

   9:         HttpResponseMessage response = null;

   

  10:         if (!context.IsPreflight)

   

  11:         {

   

  12:             response = await base.SendAsync(request, cancellationToken);

   

  13:         }

   

  14:

   

  15:         //利用註冊的CorsPolicyProviderFactory得到對應的CorsPolicyProvider

   

  16:         //藉助於CorsPolicyProvider得到表示CORS資源授權策略的CorsPolicy

   

  17:         HttpConfiguration configuration = request.GetConfiguration();

   

  18:         CorsPolicy policy = await configuration.GetCorsPolicyProviderFactory().GetCorsPolicyProvider(request).GetCorsPolicyAsync(request,cancellationToken);

   

  19:

   

  20:         //獲取註冊的CorsEngine

   

  21:         //利用CorsEngine對請求實施CORS資源授權檢驗,並得到表示檢驗結果的CorsResult物件

   

  22:         ICorsEngine engine = configuration.GetCorsEngine();

   

  23:         CorsResult result = engine.EvaluatePolicy(context, policy);

   

  24:

   

  25:         //針對預檢請求

   

  26:         //如果請求透過授權檢驗,返回一個狀態為“200, OK”的響應並新增CORS報頭

   

  27:         //如果授權檢驗失敗,返回一個狀態為“400, Bad Request”的響應並指定授權失敗原因

   

  28:         if (context.IsPreflight)

   

  29:         {

   

  30:             if (result.IsValid)

   

  31:             {

   

  32:                 response = new HttpResponseMessage(HttpStatusCode.OK);

   

  33:                 response.AddCorsHeaders(result);

   

  34:             }

   

  35:             else

   

  36:             {

   

  37:                 response = request.CreateErrorResponse(HttpStatusCode.BadRequest,string.Join(" |", result.ErrorMessages.ToArray()));

   

  38:             }

   

  39:         }

   

  40:         //針對非預檢請求

   

  41:         //CORS報頭只有在透過授權檢驗情況下才會被新增到響應報頭集合中

   

  42:         else if (result.IsValid)

   

  43:         {

   

  44:             response.AddCorsHeaders(result);

   

  45:         }

   

  46:         return response;

   

  47:     }

   

  48: }

如上面的程式碼片斷所示,我們首選在實現的SendAsync方法中呼叫自定義的擴充套件方法CreateCorsRequestContext根據表示當前請求的HttpRequestMessge物件建立出表示針對CORS的跨域資源請求上下文的CorsRequestContext物件。

然後我們根據CorsRequestContext的IsPreflight屬性判斷當前是否是一個預檢請求。對於預檢請求,我們會直接呼叫基類的同名方法將請求傳遞給訊息處理管道的後續環節作進一步處理,並最終得到表示響應的HttpResponse物件。

我們接下來從表示當前請求的HttpRequestMessge物件中直接獲取當前HttpConfiguration物件,並呼叫擴充套件方法GetCorsPolicyProviderFactory得到註冊在它上面的CorsPolicyProviderFactory,進而得到由它提供的GetCorsPolicyProvider。透過呼叫此GetCorsPolicyProvider的方法GetCorsPolicyAsync,我們會得到目標Action方法採用的CORS資源授權策略,這是一個CorsPolicy物件。

在這之後,我們呼叫HttpConfiguration物件的另一個擴充套件方法GetCorsEngine得到註冊其上的CorsEngine,並將此前得到的CorsRequestContext和CorsPolicy物件作為引數呼叫它的方法EvaluatePolicy由此開始針對當前請求的CORS資源授權檢驗,並最終得到表示檢驗結果的CorsResult。

透過CorsResult的IsValid屬性表示當前請求是否透過CORS資源授權檢驗。對於預檢請求,在請求透過授權檢驗的情況下,我們會建立一個狀態為“200, OK”的HttpResponseMessage作為最終的響應,在返回之前我們呼叫自定義的擴充套件方法AddCorsHeaders將從CorsResult得到的CORS響應報頭新增到此HttpResponseMessage的報頭集合中。如果請求沒有透過授權檢驗,我們會返回一個狀態為“400, Bad Request”的響應,透過CorsResult的ErrorMessage屬性提取的錯誤訊息(表示授權失敗的原因)會作為響應的主體內容。

對於非預檢請求來說,只有在它透過了資源授權檢驗的情況下,我們才會呼叫擴充套件方法AddCorsHeaders將從CorsResult得到的CORS報頭新增響應的報頭集合中。換句話說,對於未取得授權的非預檢跨域資源請求,MyCorsMessageHandler沒有對響應作任何的改變。

如下所示的是分別針對HttpRequestMessage和HttpResponseMessage定義的兩個擴充套件方法,其中CreateCorsRequestContext方法根據HttpRequestMessage建立CorsRequestContext物件,而AddCorsHeaders方法則將從CorsResult中獲取的CORS響應報頭新增到指定的HttpResponseMessage中。

   1: public static class CorsExtensions

   

   2: {

   

   3:     public static CorsRequestContext CreateCorsRequestContext(this HttpRequestMessage request)

   

   4:     {

   

   5:         CorsRequestContext context = new CorsRequestContext

   

   6:         {

   

   7:             RequestUri = request.RequestUri,

   

   8:             HttpMethod = request.Method.Method,

   

   9:             Host = request.Headers.Host,

   

  10:             Origin = request.GetHeader("Origin"),

   

  11:             AccessControlRequestMethod = request.GetHeader("Access-Control-Request-Method")

   

  12:         };

   

  13:

   

  14:         string requestHeaders = request.GetHeader("Access-Control-Request-Headers");

   

  15:         if (!string.IsNullOrEmpty(requestHeaders))

   

  16:         {

   

  17:             Array.ForEach(requestHeaders.Split(','), header => context.AccessControlRequestHeaders.Add(header.Trim()));

   

  18:         }

   

  19:         return context;

   

  20:     }

   

  21:

   

  22:     public static void AddCorsHeaders(this HttpResponseMessage response, CorsResult result)

   

  23:     {

   

  24:         foreach (var item in result.ToResponseHeaders())

   

  25:         {

   

  26:             response.Headers.TryAddWithoutValidation(item.Key, item.Value);

   

  27:         }

   

  28:     }

   

  29:

   

  30:     private static string GetHeader(this HttpRequestMessage request, string name)

   

  31:     {

   

  32:         IEnumerable<string> headerValues;

   

  33:         if (request.Headers.TryGetValues(name, out headerValues))

   

  34:         {

   

  35:             return headerValues.FirstOrDefault();

   

  36:         }

   

  37:         return null;

   

  38:     }

   

  39: }

為了驗證我們這個用於模擬CorsMessageHandler的自定義HttpMessageHandler是否能夠真正為ASP.NET Web API提供針對CORS的支援,我們直接將其應用到《同源策略與JSONP》建立的演示例項中。我們透過上面介紹的方式為WebApi應用安裝“Microsoft ASP.NET Web API 2 Cross-Origin Support”這個NuGet包後,將EnableCorsAttribute特性應用到定義在ContactsController上並作如下的設定。

   1: [EnableCors("","*","*")]

   

   2: public class ContactsController : ApiController

   

   3: {

   

   4:     public IHttpActionResult GetAllContacts()

   

   5:     {

   

   6:         //省略實現

   

   7:     }

   

   8: }

在Global.asax中,我們並不呼叫當前HttpConfiguration的EnableCors方法開啟ASP.NET Web API針對CORS的支援,而是採用如下的方式將建立的CorsMessageHandler物件新增到訊息處理管道中。如果現在執行ASP.NET MVC程式,透過呼叫Web API以跨域Ajax請求得到的聯絡人列表依然會顯示在瀏覽器上。

   1: public class WebApiApplication : System.Web.HttpApplication

   

   2: {

   

   3:     protected void Application_Start()

   

   4:     {

   

   5:         GlobalConfiguration.Configuration.MessageHandlers.Add(new MyCorsMessageHandler());

   

   6:         //其他操作

   

   7:     }

   

   8: }

HttpConfiguration的EnableCors方法

透過上面的介紹我們知道針對ASP.NET Web API的CORS程式設計首先需要做的就是在程式啟動之前呼叫當前HttpConfiguration的擴充套件方法EnableCors開啟對CORS的支援,那麼該方法中具體實現了怎樣操作呢?由於ASP.NET Web API針對CORS的支援最終是透過CorsMesssageHandler這個自定義的HttpMessageHandler來實現的,所以對於HttpConfiguration的擴充套件方法EnableCors來說,其核心操作就是對CorsMesssageHandler予以註冊。

   1: public static class CorsHttpConfigurationExtensions

   

   2: {

   

   3:     public static void EnableCors(this HttpConfiguration httpConfiguration);

   

   4:     public static void EnableCors(this HttpConfiguration httpConfiguration, ICorsPolicyProvider defaultPolicyProvider);

   

   5: }

   

   6:

   

   7: public class AttributeBasedPolicyProviderFactory : ICorsPolicyProviderFactory

   

   8: {

   

   9:     //其他成員

   

  10:     public ICorsPolicyProvider DefaultPolicyProvider { get; set; }

   

  11: }

如上面的程式碼片斷所示,HttpConfiguration具有兩個過載的EnableCors方法。其中一個可以指定一個預設的CorsPolicyProvider,如果呼叫此方法並指定一個具體的CorsPolicyProvider物件,一個AttributeBasedPolicyProviderFactory物件會被建立出來並註冊到HttpConfiguration上。而指定的CorsPolicyProvider實際上會作為AttributeBasedPolicyProviderFactory物件的DefaultPolicyProvider屬性。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2983/viewspace-2817875/,如需轉載,請註明出處,否則將追究法律責任。

相關文章