跨域资源共享(CORS)在ASP.NET Web API中是如何实现的?

在《通过扩展让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("http://localhost:9527","*","*")] 
   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属性。

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

posted @ 2013-12-16 08:29  Artech  阅读(11253)  评论(16编辑  收藏