首页 > 解决方案 > CORS、预检选项、IIS 和 WebApi2

问题描述

对另一台服务器上的 WebApi2 端点的 SPA 提取调用停止工作。这两个人多年来一直是朋友,但我最近重新发布了两端,所以事情有点模棱两可。

Edge 在这方面比 Chrome 更有帮助,它告诉我我收到了 405 响应 OPTIONS 请求。对 SO 和整个网络的翻阅让我想起了在由 Kestrel 托管的 SPA 中设置 CORS,并且查看源代码表明我已经这样做了。查看请求标头会显示具有预期值的所需标头。这并不奇怪,因为对 OPTIONS 请求获得 405 响应意味着 SPA 正在发送一个,这意味着它已适当配置。

更多的翻找发现我应该将 NuGet 包添加Microsoft.AspNet.WebApi.Cors到 WebApi 项目中。检查显示它已经存在。

查看 WebApi 的 web.config 发现了这一点

<handlers>
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <remove name="OPTIONSVerbHandler" />
  <remove name="TRACEVerbHandler" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

我曾相信 WebApi 端的 CORS 包的目的是处理 OPTIONS 动词。但是 405 响应说这没有发生,所以我注释掉了<remove name="OPTIONSVerbHandler" />,Lo!他们又是朋友了。

问题解决了?并不真地。在应用程序中处理这个问题的目的是减少对外部定义的配置选项的依赖。这让我想到了这个问题:

Microsoft.AspNet.WebApi.Cors除了使用 NuGet添加之外,您还需要做什么?

这是一个 WebApi2 服务,当 CORS 不在老朋友之间时,它工作得很好。它在 .NET Framework 4.7.2 上运行,并托管在具有当前服务包的 Server 2012 上的 IIS 上。客户端和服务器软件都在专用网络上运行,因此 CORS 引入了复杂性来解决我们没有的问题。知道如何让它闭嘴并停止帮助是非常有用的。

标签: c#corsasp.net-web-api2

解决方案


我在Brock Allen 的 MSDN 文章中找到了我的问题的答案。

根据关于仅链接答案的 SO 政策,在下面复制了回答此问题的文章部分,以防止链接中断。我想强调一下,以下不是我的工作,完全归功于 thinktecture 的 Brock Allen。


Web API 2 中的 CORS 支持

Web API 中的 CORS 支持是一个完整的框架,允许应用程序定义 CORS 请求的权限。该框架围绕策略的概念展开,该策略允许您为应用程序的任何给定请求指定允许的 CORS 功能。

首先,为了获得 CORS 框架,您必须从 Web API 应用程序中引用 CORS 库(默认情况下,它们不会从 Visual Studio 2013 中的任何 Web API 模板中引用)。Web API CORS 框架可通过 NuGet 作为 Microsoft.AspNet.WebApi.Cors 包获得。如果您不使用 NuGet,它也可以作为 Visual Studio 2013 的一部分使用,您需要引用两个程序集:System.Web.Http.Cors.dll 和 System.Web.Cors.dll(在我的机器上这些位于 C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Stack 5\Packages)。

接下来,为了表达策略,Web API 提供了一个名为 EnableCorsAttribute 的自定义属性类。此类包含允许的来源、HTTP 方法、请求标头、响应标头以及是否允许凭据的属性(它模拟了前面讨论的 CORS 规范的所有细节)。

最后,为了让 Web API CORS 框架能够处理 CORS 请求并发出适当的 CORS 响应标头,它必须查看应用程序中的每个请求。Web API 具有通过消息处理程序进行此类拦截的扩展点。相应地,Web API CORS 框架实现了一个名为 CorsMessageHandler 的消息处理程序。对于 CORS 请求,它将查询属性中表示的策略以用于被调用的方法并发出适当的 CORS 响应标头。

EnableCorsAttribute EnableCorsAttribute 类是应用程序表达其 CORS 策略的方式。EnableCorsAttribute 类有一个可以接受三个或四个参数的重载构造函数。参数(按顺序)是:

  1. 允许的来源列表
  2. 允许的请求标头列表
  3. 允许的 HTTP 方法列表
  4. 允许的响应头列表(可选)

还有一个属性用于允许凭据 (SupportsCredentials) 和另一个用于指定预检缓存持续时间值 (PreflightMaxAge)。

图 2显示了将 EnableCors 属性应用于控制器上的各个方法的示例。用于各种 CORS 策略设置的值应与前面示例中显示的 CORS 请求和响应相匹配。

图 2 将 EnableCors 属性应用于操作方法

public class ResourcesController : ApiController
{
  [EnableCors("http://localhost:55912", // Origin
              null,                     // Request headers
              "GET",                    // HTTP methods
              "bar",                    // Response headers
              SupportsCredentials=true  // Allow credentials
  )]
  public HttpResponseMessage Get(int id)
  {
    var resp = Request.CreateResponse(HttpStatusCode.NoContent);
    resp.Headers.Add("bar", "a bar value");
    return resp;
  }
  [EnableCors("http://localhost:55912",       // Origin
              "Accept, Origin, Content-Type", // Request headers
              "PUT",                          // HTTP methods
              PreflightMaxAge=600             // Preflight cache duration
  )]
  public HttpResponseMessage Put(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
  [EnableCors("http://localhost:55912",       // Origin
              "Accept, Origin, Content-Type", // Request headers
              "POST",                         // HTTP methods
              PreflightMaxAge=600             // Preflight cache duration
  )]
  public HttpResponseMessage Post(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
}

请注意,每个构造函数参数都是一个字符串。通过指定一个逗号分隔的列表来指示多个值(如图 2中为允许的请求标头所指定的那样)。如果您希望允许所有来源、请求标头或 HTTP 方法,您可以使用“*”作为值(对于响应标头您仍然必须是显式的)。

除了在方法级别应用 EnableCors 属性外,您还可以在类级别或全局将其应用到应用程序。应用属性的级别会在您的 Web API 代码中为该级别及以下级别的所有请求配置 CORS。因此,例如,如果在方法级别应用,该策略将仅适用于该操作的请求,而如果在类级别应用,该策略将适用于对该控制器的所有请求。最后,如果在全局范围内应用,该策略将适用于所有请求。

以下是在类级别应用属性的另一个示例。此示例中使用的设置非常宽松,因为通配符用于允许的来源、请求标头和 HTTP 方法:

[EnableCors("*", "*", "*")]
public class ResourcesController : ApiController
{
  public HttpResponseMessage Put(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
  public HttpResponseMessage Post(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
}

如果在多个位置有一个策略,则使用“最近”属性并忽略其他属性(因此优先级是方法,然后是类,然后是全局)。如果您已在较高级别应用了策略,但又希望在较低级别排除请求,则可以使用另一个名为 DisableCorsAttribute 的属性类。该属性本质上是一个不允许任何权限的策略。

如果您在控制器上有其他不想允许 CORS 的方法,则可以使用两个选项之一。首先,您可以在 HTTP 方法列表中显式,如图 3所示。或者,您可以保留通配符,但排除具有 DisableCors 属性的 Delete 方法,如图 4所示。

图 3 为 HTTP 方法使用显式值

[EnableCors("*", "*", "PUT, POST")]
public class ResourcesController : ApiController
{
  public HttpResponseMessage Put(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
  public HttpResponseMessage Post(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
  // CORS not allowed because DELETE is not in the method list above
  public HttpResponseMessage Delete(int id)
  {
    return Request.CreateResponse(HttpStatusCode.NoContent);
  }
}

图 4 使用 DisableCors 属性

[EnableCors("*", "*", "*")]
public class ResourcesController : ApiController
{
  public HttpResponseMessage Put(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
  public HttpResponseMessage Post(Resource data)
  {
    return Request.CreateResponse(HttpStatusCode.OK, data);
  }
  // CORS not allowed because of the [DisableCors] attribute
  [DisableCors]
  public HttpResponseMessage Delete(int id)
  {
    return Request.CreateResponse(HttpStatusCode.NoContent);
  }
}

CorsMessageHandler必须为 CORS 框架启用 CorsMessageHandler 才能执行拦截请求以评估 CORS 策略并发出 CORS 响应标头的工作。启用消息处理程序通常在应用程序的 Web API 配置类中通过调用 EnableCors 扩展方法来完成:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // Other configuration omitted
    config.EnableCors();
  }
}

如果您希望提供全局 CORS 策略,可以将 EnableCorsAttribute 类的实例作为参数传递给 EnableCors 方法。例如,以下代码将在应用程序中全局配置许可 CORS 策略:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // Other configuration omitted
    config.EnableCors(new EnableCorsAttribute("*", "*", "*"));
  }
}

与任何消息处理程序一样,CorsMessageHandler 也可以按路由而不是全局注册。

以上就是 ASP.NET Web API 2 中基本的、“开箱即用”的 CORS 框架。该框架的一个优点是它可以针对更多动态场景进行扩展,我将在接下来介绍这一点。

定制策略

从前面的示例中可以明显看出,来源列表(如果未使用通配符)是编译到 Web API 代码中的静态列表。虽然这可能在开发期间或特定场景中起作用,但如果需要动态确定来源(或其他权限)列表(例如,从数据库中),这还不够。

幸运的是,Web API 中的 CORS 框架是可扩展的,因此支持动态来源列表很容易。事实上,该框架非常灵活,有两种通用的方法来定制策略的生成。

自定义 CORS 策略属性启用动态 CORS 策略的一种方法是开发一个自定义属性类,该类可以从某些数据源生成策略。可以使用此自定义属性类代替 Web API 提供的 EnableCorsAttribute 类。这种方法很简单,并且保留了能够根据需要将属性应用于特定类和方法(而不是将其应用于其他方法)的细粒度感觉。

要实现此方法,您只需构建一个类似于现有 EnableCorsAttribute 类的自定义属性。主要关注点是 ICorsPolicyProvider 接口,它负责为任何给定请求创建 CorsPolicy 的实例。图 5 包含一个示例。

图 5 自定义 CORS 策略属性

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
                AllowMultiple = false)]
public class EnableCorsForPaidCustomersAttribute :
  Attribute, ICorsPolicyProvider
{
  public async Task<CorsPolicy> GetCorsPolicyAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var corsRequestContext = request.GetCorsRequestContext();
    var originRequested = corsRequestContext.Origin;
    if (await IsOriginFromAPaidCustomer(originRequested))
    {
      // Grant CORS request
      var policy = new CorsPolicy
      {
        AllowAnyHeader = true,
        AllowAnyMethod = true,
      };
      policy.Origins.Add(originRequested);
      return policy;
    }
    else
    {
      // Reject CORS request
      return null;
    }
  }
  private async Task<bool> IsOriginFromAPaidCustomer(
    string originRequested)
  {
    // Do database look up here to determine if origin should be allowed
    return true;
  }
}

CorsPolicy 类具有表达要授予的 CORS 权限的所有属性。这里使用的值只是一个示例,但可能它们可以从数据库查询(或任何其他来源)动态填充。

自定义策略提供者工厂构建动态 CORS 策略的第二种通用​​方法是创建自定义策略提供者工厂。这是获取当前请求的策略提供程序的 CORS 框架的一部分。Web API 的默认实现使用自定义属性来发现策略提供者(如您之前所见,属性类本身就是策略提供者)。这是 CORS 框架的另一个可插入部分,如果您想使用自定义属性以外的策略方法,您将实现自己的策略提供程序工厂。

前面描述的基于属性的方法提供了从请求到策略的隐式关联。自定义策略提供者工厂方法与属性方法不同,因为它要求您的实现提供将传入请求与策略匹配的逻辑。这种方法更粗粒度,因为它本质上是一种用于获取 CORS 策略的集中方法。

图 6显示了一个自定义策略提供程序工厂的示例。此示例的主要重点是 ICorsPolicyProviderFactory 接口及其 GetCorsPolicyProvider 方法的实现。

Figure 6 A Custom Policy Provider Factory
public class DynamicPolicyProviderFactory : ICorsPolicyProviderFactory
{
  public ICorsPolicyProvider GetCorsPolicyProvider(
    HttpRequestMessage request)
  {
    var route = request.GetRouteData();
    var controller = (string)route.Values["controller"];
    var corsRequestContext = request.GetCorsRequestContext();
    var originRequested = corsRequestContext.Origin;
    var policy = GetPolicyForControllerAndOrigin(
      controller, originRequested);
    return new CustomPolicyProvider(policy);
  }
  private CorsPolicy GetPolicyForControllerAndOrigin(
   string controller, string originRequested)
  {
    // Do database lookup to determine if the controller is allowed for
    // the origin and create CorsPolicy if it is (otherwise return null)
    var policy = new CorsPolicy();
    policy.Origins.Add(originRequested);
    policy.Methods.Add("GET");
    return policy;
  }
}
public class CustomPolicyProvider : ICorsPolicyProvider
{
  CorsPolicy policy;
  public CustomPolicyProvider(CorsPolicy policy)
  {
    this.policy = policy;
  }
  public Task<CorsPolicy> GetCorsPolicyAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
  {
    return Task.FromResult(this.policy);
  }
}

这种方法的主要区别在于,完全取决于实现来确定来自传入请求的策略。在图 6 中,控制器和源可用于查询数据库以获取策略值。同样,这种方法是最灵活的,但它可能需要更多的工作来根据请求确定策略。

要使用自定义策略提供者工厂,您必须通过 Web API 配置中的 SetCorsPolicyProviderFactory 扩展方法将其注册到 Web API:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // Other configuration omitted
    config.EnableCors();
    config.SetCorsPolicyProviderFactory(
      new DynamicPolicyProviderFactory());
  }
}

调试 CORS

如果(以及何时)你的跨域 AJAX 调用不起作用,我会想到一些技术来调试 CORS。

客户端一种调试方法是简单地使用您选择的 HTTP 调试器(例如 Fiddler)并检查所有 HTTP 请求。有了前面收集到的有关 CORS 规范细节的知识,您通常可以通过检查 CORS HTTP 标头(或缺少标头)来找出未授予特定 AJAX 请求权限的原因。

另一种方法是使用浏览器的 F12 开发人员工具。当 AJAX 调用因 CORS 而失败时,现代浏览器中的控制台窗口会提供有用的错误消息。

服务器端CORS 框架本身使用 Web API 的跟踪工具提供详细的跟踪消息。只要向 Web API 注册了 ITraceWriter,CORS 框架就会发出消息,其中包含有关所选策略提供者、使用的策略以及发出的 CORS HTTP 标头的信息。有关 Web API 跟踪的更多信息,请参阅 MSDN 上的 Web API 文档。


我想强调一下,以上摘录不是我的作品,完全归功于 thinktecture 的 Brock Allen。


推荐阅读