在《通過擴(kuò)展讓ASP.NET Web API支持W3C的CORS規(guī)范》中,我們通過自定義的HttpMessageHandler自行為ASP.NET Web API實(shí)現(xiàn)了針對CORS的支持,實(shí)際上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的核心功能在于:提取預(yù)定義的CORS授權(quán)策略并對當(dāng)前請求實(shí)施授權(quán)檢驗(yàn),并根據(jù)授權(quán)檢驗(yàn)的結(jié)果為現(xiàn)有的響應(yīng)(針對簡單跨域資源請求和繼預(yù)檢請求之后發(fā)送的真正跨域資源請求)或者新創(chuàng)建的響應(yīng)(針對預(yù)檢請求)添加相應(yīng)的CORS報(bào)頭。如上面的代碼片斷所示,CorsMessageHandler定義了HandleCorsPreflightRequestAsync和HandleCorsRequestAsync虛方法,它們分別實(shí)現(xiàn)針對預(yù)檢請求和非預(yù)檢請求的CORS授權(quán)檢驗(yàn)。
在實(shí)現(xiàn)的SendAsync方法中,當(dāng)CorsRequestContext根據(jù)表示當(dāng)前請求的HttpRequestMessage對象創(chuàng)建之后,會(huì)根據(jù)其IsPreflight屬性選擇調(diào)用方法HandleCorsPreflightRequestAsync或者HandleCorsRequestAsync。
CORS授權(quán)檢驗(yàn)
實(shí)現(xiàn)在CorsMessageHandler中的具體CORS授權(quán)檢驗(yàn)流程基本上體現(xiàn)在右圖中。它首先根據(jù)表示當(dāng)前請求的HttpRequestMessage對象創(chuàng)建CorsRequestContext對象。然后利用注冊的CorsProviderFactory得到對應(yīng)的CorsProvider對象,并利用后者得到針對當(dāng)前請求的資源授權(quán)策略,這是一個(gè)CorsPolicy對象。
接下來,CorsMessageHandler會(huì)獲取注冊的CorsEngine。此前得到的CorsRequestContext和CorsPolicy對象會(huì)作為參數(shù)調(diào)用CorsEngine的EvaluatePolicy方法,CORS資源授權(quán)檢驗(yàn)由此開始。授權(quán)檢驗(yàn)結(jié)束之后,CorsMessageHandler會(huì)得到表示檢驗(yàn)結(jié)果的CorsResult對象。
對于預(yù)檢請求,CorsMessageHandler會(huì)直接創(chuàng)建HttpResponseMessage對象予以響應(yīng)。具體來說,如果預(yù)檢請求通過了授權(quán)檢驗(yàn),一個(gè)狀態(tài)為“200, OK”的HttpResponseMessage會(huì)被創(chuàng)建出來,通過CorsResult得到CORS響應(yīng)報(bào)頭會(huì)被添加到這個(gè)HttpResponseMessage對象的報(bào)頭集合中。如果授權(quán)檢驗(yàn)失敗,創(chuàng)建的HttpResponseMessage具有的狀態(tài)為“400, Bad Request”,CorsResult攜帶的錯(cuò)誤響應(yīng)會(huì)作為響應(yīng)的主體內(nèi)容。
對于非預(yù)檢請求,它會(huì)將當(dāng)前請求傳遞給消息處理管道的后續(xù)部分進(jìn)行進(jìn)一步處理,并最終得到表示響應(yīng)消息的HttpResponseMessage。只有在請求通過授權(quán)檢查的情況下,由CorsResult得到的CORS響應(yīng)報(bào)頭才會(huì)被添加到此HttpResponseMessage的報(bào)頭集合中。
實(shí)例演示:創(chuàng)建MyCorsMessageHandler模擬具體采用的授權(quán)檢驗(yàn)
為了讓讀者朋友們對實(shí)現(xiàn)在CorsMessageHandler中的具體CORS資源授權(quán)流程具有更加深刻的認(rèn)識(shí),我們現(xiàn)在將這樣的授權(quán)檢驗(yàn)邏輯實(shí)現(xiàn)在一個(gè)自定義的HttpMessageHandler中。為此我們定義了如下一個(gè)MyCorsMessageHandler類型,由于它僅僅用于模擬CorsMessageHandler大體實(shí)現(xiàn)邏輯,所以我們會(huì)忽略很多細(xì)節(jié)上(比如異常處理)的代碼。
1: public class MyCorsMessageHandler: DelegatingHandler
2: {
3: protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
4: {
5: //根據(jù)當(dāng)前請求創(chuàng)建CorsRequestContext
6: CorsRequestContext context = request.CreateCorsRequestContext();
7:
8: //針對非預(yù)檢請求:將請求傳遞給消息處理管道后續(xù)部分繼續(xù)處理,并得到響應(yīng)
9: HttpResponseMessage response = null;
10: if (!context.IsPreflight)
11: {
12: response = await base.SendAsync(request, cancellationToken);
13: }
14:
15: //利用注冊的CorsPolicyProviderFactory得到對應(yīng)的CorsPolicyProvider
16: //借助于CorsPolicyProvider得到表示CORS資源授權(quán)策略的CorsPolicy
17: HttpConfiguration configuration = request.GetConfiguration();
18: CorsPolicy policy = await configuration.GetCorsPolicyProviderFactory().GetCorsPolicyProvider(request).GetCorsPolicyAsync(request,cancellationToken);
19:
20: //獲取注冊的CorsEngine
21: //利用CorsEngine對請求實(shí)施CORS資源授權(quán)檢驗(yàn),并得到表示檢驗(yàn)結(jié)果的CorsResult對象
22: ICorsEngine engine = configuration.GetCorsEngine();
23: CorsResult result = engine.EvaluatePolicy(context, policy);
24:
25: //針對預(yù)檢請求
26: //如果請求通過授權(quán)檢驗(yàn),返回一個(gè)狀態(tài)為“200, OK”的響應(yīng)并添加CORS報(bào)頭
27: //如果授權(quán)檢驗(yàn)失敗,返回一個(gè)狀態(tài)為“400, Bad Request”的響應(yīng)并指定授權(quán)失敗原因
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: //針對非預(yù)檢請求
41: //CORS報(bào)頭只有在通過授權(quán)檢驗(yàn)情況下才會(huì)被添加到響應(yīng)報(bào)頭集合中
42: else if (result.IsValid)
43: {
44: response.AddCorsHeaders(result);
45: }
46: return response;
47: }
48: }
如上面的代碼片斷所示,我們首選在實(shí)現(xiàn)的SendAsync方法中調(diào)用自定義的擴(kuò)展方法CreateCorsRequestContext根據(jù)表示當(dāng)前請求的HttpRequestMessge對象創(chuàng)建出表示針對CORS的跨域資源請求上下文的CorsRequestContext對象。
然后我們根據(jù)CorsRequestContext的IsPreflight屬性判斷當(dāng)前是否是一個(gè)預(yù)檢請求。對于預(yù)檢請求,我們會(huì)直接調(diào)用基類的同名方法將請求傳遞給消息處理管道的后續(xù)環(huán)節(jié)作進(jìn)一步處理,并最終得到表示響應(yīng)的HttpResponse對象。
我們接下來從表示當(dāng)前請求的HttpRequestMessge對象中直接獲取當(dāng)前HttpConfiguration對象,并調(diào)用擴(kuò)展方法GetCorsPolicyProviderFactory得到注冊在它上面的CorsPolicyProviderFactory,進(jìn)而得到由它提供的GetCorsPolicyProvider。通過調(diào)用此GetCorsPolicyProvider的方法GetCorsPolicyAsync,我們會(huì)得到目標(biāo)Action方法采用的CORS資源授權(quán)策略,這是一個(gè)CorsPolicy對象。
在這之后,我們調(diào)用HttpConfiguration對象的另一個(gè)擴(kuò)展方法GetCorsEngine得到注冊其上的CorsEngine,并將此前得到的CorsRequestContext和CorsPolicy對象作為參數(shù)調(diào)用它的方法EvaluatePolicy由此開始針對當(dāng)前請求的CORS資源授權(quán)檢驗(yàn),并最終得到表示檢驗(yàn)結(jié)果的CorsResult。
通過CorsResult的IsValid屬性表示當(dāng)前請求是否通過CORS資源授權(quán)檢驗(yàn)。對于預(yù)檢請求,在請求通過授權(quán)檢驗(yàn)的情況下,我們會(huì)創(chuàng)建一個(gè)狀態(tài)為“200, OK”的HttpResponseMessage作為最終的響應(yīng),在返回之前我們調(diào)用自定義的擴(kuò)展方法AddCorsHeaders將從CorsResult得到的CORS響應(yīng)報(bào)頭添加到此HttpResponseMessage的報(bào)頭集合中。如果請求沒有通過授權(quán)檢驗(yàn),我們會(huì)返回一個(gè)狀態(tài)為“400, Bad Request”的響應(yīng),通過CorsResult的ErrorMessage屬性提取的錯(cuò)誤消息(表示授權(quán)失敗的原因)會(huì)作為響應(yīng)的主體內(nèi)容。
對于非預(yù)檢請求來說,只有在它通過了資源授權(quán)檢驗(yàn)的情況下,我們才會(huì)調(diào)用擴(kuò)展方法AddCorsHeaders將從CorsResult得到的CORS報(bào)頭添加響應(yīng)的報(bào)頭集合中。換句話說,對于未取得授權(quán)的非預(yù)檢跨域資源請求,MyCorsMessageHandler沒有對響應(yīng)作任何的改變。
如下所示的是分別針對HttpRequestMessage和HttpResponseMessage定義的兩個(gè)擴(kuò)展方法,其中CreateCorsRequestContext方法根據(jù)HttpRequestMessage創(chuàng)建CorsRequestContext對象,而AddCorsHeaders方法則將從CorsResult中獲取的CORS響應(yīng)報(bào)頭添加到指定的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: }
為了驗(yàn)證我們這個(gè)用于模擬CorsMessageHandler的自定義HttpMessageHandler是否能夠真正為ASP.NET Web API提供針對CORS的支持,我們直接將其應(yīng)用到《同源策略與JSONP》創(chuàng)建的演示實(shí)例中。我們通過上面介紹的方式為WebApi應(yīng)用安裝“Microsoft ASP.NET Web API 2 Cross-Origin Support”這個(gè)NuGet包后,將EnableCorsAttribute特性應(yīng)用到定義在ContactsController上并作如下的設(shè)置。
1: [EnableCors("http://localhost:9527","*","*")]
2: public class ContactsController : ApiController
3: {
4: public IHttpActionResult GetAllContacts()
5: {
6: //省略實(shí)現(xiàn)
7: }
8: }
在Global.asax中,我們并不調(diào)用當(dāng)前HttpConfiguration的EnableCors方法開啟ASP.NET Web API針對CORS的支持,而是采用如下的方式將創(chuàng)建的CorsMessageHandler對象添加到消息處理管道中。如果現(xiàn)在運(yùn)行ASP.NET MVC程序,通過調(diào)用Web API以跨域Ajax請求得到的聯(lián)系人列表依然會(huì)顯示在瀏覽器上。
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編程首先需要做的就是在程序啟動(dòng)之前調(diào)用當(dāng)前HttpConfiguration的擴(kuò)展方法EnableCors開啟對CORS的支持,那么該方法中具體實(shí)現(xiàn)了怎樣操作呢?由于ASP.NET Web API針對CORS的支持最終是通過CorsMesssageHandler這個(gè)自定義的HttpMessageHandler來實(shí)現(xiàn)的,所以對于HttpConfiguration的擴(kuò)展方法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具有兩個(gè)重載的EnableCors方法。其中一個(gè)可以指定一個(gè)默認(rèn)的CorsPolicyProvider,如果調(diào)用此方法并指定一個(gè)具體的CorsPolicyProvider對象,一個(gè)AttributeBasedPolicyProviderFactory對象會(huì)被創(chuàng)建出來并注冊到HttpConfiguration上。而指定的CorsPolicyProvider實(shí)際上會(huì)作為AttributeBasedPolicyProviderFactory對象的DefaultPolicyProvider屬性。