在 ASP.NET Web API 2 中启用跨域请求

作者:Mike Wasson

此内容适用于早期版本的 .NET。 新开发应使用 ASP.NET Core。 有关在 ASP.NET Core 中使用 Web API 和跨源请求(CORS)的详细信息,请参阅:

浏览器安全性将阻止网页向另一个域发出 AJAX 请求。 此限制称为“同源策略”,可防止恶意站点从另一站点读取敏感数据。 但有时你可能想让其他站点调用你的 Web API。

跨域资源共享 (CORS)是一种 W3C 标准,允许服务器放宽同源策略。 使用 CORS,服务器可以显式允许某些跨域请求,同时拒绝另一些跨域请求。 CORS 比早期技术(如 JSONP)更安全、更灵活。 本教程介绍如何在 Web API 应用程序中启用 CORS。

本教程中使用的软件

介绍

本教程演示 ASP.NET Web API 中的 CORS 支持。 首先,我们将创建两个 ASP.NET 项目-一个名为“WebService”,其中一个名为“WebService”,后者托管 Web API 控制器,另一个称为“WebClient”,后者调用 WebService。 由于这两个应用程序托管在不同的域中,因此从 WebClient 到 WebService 的 AJAX 请求是跨源请求。

显示 Web 服务和 Web 客户端

什么是“同源”?

如果两个 URL 具有相同的方案、主机和端口,则其源相同。 (RFC 6454

这两个 URL 同源:

  • http://example.com/foo.html
  • http://example.com/bar.html

与前两个 URL 相比,这些 URL 具有不同的源:

  • http://example.net - 不同的域
  • http://example.com:9000/foo.html - 不同的端口
  • https://example.com/foo.html - 不同的方案
  • http://www.example.com/foo.html - 不同的子域

注意

比较源时,Internet Explorer 不考虑端口。

创建 WebService 项目

注意

本部分假定你已了解如何创建 Web API 项目。 如果没有,请参阅 ASP.NET Web API 入门。

  1. 启动 Visual Studio 并创建新的 ASP.NET Web 应用程序(.NET Framework) 项目。

  2. “新建 ASP.NET Web 应用程序 ”对话框中,选择“ 项目模板”。 在 “添加文件夹和核心引用”下,选中 “Web API ”复选框。

    Visual Studio 中的“新建 ASP.NET 项目”对话框

  3. 添加包含以下代码的 TestController Web API 控制器:

    using System.Net.Http;
    using System.Web.Http;
    
    namespace WebService.Controllers
    {
        public class TestController : ApiController
        {
            public HttpResponseMessage Get()
            {
                return new HttpResponseMessage()
                {
                    Content = new StringContent("GET: Test message")
                };
            }
    
            public HttpResponseMessage Post()
            {
                return new HttpResponseMessage()
                {
                    Content = new StringContent("POST: Test message")
                };
            }
    
            public HttpResponseMessage Put()
            {
                return new HttpResponseMessage()
                {
                    Content = new StringContent("PUT: Test message")
                };
            }
        }
    }
    
  4. 可以在本地运行应用程序或部署到 Azure。 (对于本教程中的屏幕截图,应用部署到Azure App 服务 Web 应用。若要验证 Web API 是否正常工作,请导航到http://hostname/api/test/主机名是在其中部署应用程序的域。 应会看到响应文本“GET: Test Message”。

    显示测试消息的 Web 浏览器

创建 WebClient 项目

  1. 创建另一个 ASP.NET Web 应用程序 (.NET Framework) 项目并选择 MVC 项目模板。 (可选)选择“更改身份验证>无身份验证”。 本教程不需要身份验证。

    Visual Studio 中的“新建 ASP.NET 项目”对话框中的 MVC 模板

  2. 解决方案资源管理器中,打开文件 Views/Home/Index.cshtml。 将此文件中的代码替换为以下内容:

    <div>
        <select id="method">
            <option value="get">GET</option>
            <option value="post">POST</option>
            <option value="put">PUT</option>
        </select>
        <input type="button" value="Try it" onclick="sendRequest()" />
        <span id='value1'>(Result)</span>
    </div>
    
    @section scripts {
    <script>
        // TODO: Replace with the URL of your WebService app
        var serviceUrl = 'http://mywebservice/api/test'; 
    
        function sendRequest() {
            var method = $('#method').val();
    
            $.ajax({
                type: method,
                url: serviceUrl
            }).done(function (data) {
                $('#value1').text(data);
            }).fail(function (jqXHR, textStatus, errorThrown) {
                $('#value1').text(jqXHR.responseText || textStatus);
            });
        }
    </script>
    }
    

    对于 serviceUrl 变量,请使用 WebService 应用的 URI。

  3. 在本地运行 WebClient 应用或将其发布到其他网站。

单击“试用”按钮时,将使用下拉列表框中列出的 HTTP 方法(GET、POST 或 PUT)将 AJAX 请求提交到 WebService 应用。 这样,便可以检查不同的跨域请求。 目前,WebService 应用不支持 CORS,因此,如果单击该按钮,将收到错误。

浏览器中出现“试用”错误

注意

如果在 Fiddler工具中观察 HTTP 流量,则会看到浏览器确实发送了 GET 请求,并且请求成功,但 AJAX 调用将返回错误。 请务必了解同源策略不会阻止浏览器 发送 请求。 而是阻止应用程序看到 响应

显示 Web 请求的 Fiddler Web 调试器

启用 CORS

现在,让我们在 WebService 应用中启用 CORS。 首先,添加 CORS NuGet 包。 在 Visual Studio 的“工具”菜单中,选择“NuGet 程序包管理器”,然后选择“程序包管理器控制台”。 在“程序包管理器控制台”窗口中,键入以下命令:

Install-Package Microsoft.AspNet.WebApi.Cors

此命令安装最新的包并更新所有依赖项,包括核心 Web API 库。 使用 -Version 标志以特定版本为目标。 CORS 包需要 Web API 2.0 或更高版本。

打开文件 App_Start/WebApiConfig.cs。 将以下代码添加到 WebApiConfig.Register 方法:

using System.Web.Http;
namespace WebService
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // New code
            config.EnableCors();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

接下来,将 [EnableCors] 属性添加到 TestController 类:

using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Cors;

namespace WebService.Controllers
{
    [EnableCors(origins: "http://mywebclient.azurewebsites.net", headers: "*", methods: "*")]
    public class TestController : ApiController
    {
        // Controller methods not shown...
    }
}

对于源参数,请使用在其中部署 WebClient 应用程序的 URI。 这允许来自 WebClient 的跨域请求,同时仍然禁止所有其他跨域请求。 稍后,我将更详细地描述 [EnableCors] 的参数

不要在源 URL 的末尾包含正斜杠。

重新部署更新的 WebService 应用程序。 无需更新 WebClient。 现在,来自 WebClient 的 AJAX 请求应成功。 允许使用 GET、PUT 和 POST 方法。

显示成功测试消息的 Web 浏览器

CORS 的工作原理

本部分介绍在 HTTP 消息级别 CORS 请求中发生的情况。 请务必了解 CORS 的工作原理,以便可以正确配置 [EnableCors] 属性,并在事情无法按预期工作的情况下进行故障排除。

CORS 规范引入了多个启用跨源请求的新 HTTP 标头。 如果浏览器支持 CORS,则会自动为跨域请求设置这些标头;无需在 JavaScript 代码中执行任何特殊操作。

下面是跨域请求的示例。 “Origin”标头为发出请求的网站提供域。

GET http://myservice.azurewebsites.net/api/test HTTP/1.1
Referer: http://myclient.azurewebsites.net/
Accept: */*
Accept-Language: en-US
Origin: http://myclient.azurewebsites.net
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: myservice.azurewebsites.net

如果服务器允许请求,则会设置 Access-Control-Allow-Origin 标头。 此标头的值与 Origin 标头匹配,或为通配符值“*”,表示允许任何源。

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Date: Wed, 05 Jun 2013 06:27:30 GMT
Content-Length: 17

GET: Test message

如果响应不包括 Access-Control-Allow-Origin 标头,AJAX 请求将失败。 具体来说,浏览器不允许该请求。 即使服务器返回成功的响应,浏览器也不会使响应对客户端应用程序可用。

预检请求

对于某些 CORS 请求,浏览器会在发送资源的实际请求之前发送一个名为“预检请求”的其他请求。

如果满足以下条件,浏览器可以跳过预检请求:

  • 请求方法为 GET、HEAD 或 POST, 以及

  • 应用程序不设置除 Accept、Accept-Language、Content-Language、Content-Type 或 Last-Event-ID 以外的任何请求标头,

  • Content-Type 标头(如果设置)是下列项之一:

    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

有关请求标头的规则适用于应用程序通过在 XMLHttpRequest 对象上调用 setRequestHeader 设置的标头。 (CORS 规范调用这些“作者请求标头”。该规则不适用于浏览器可以设置的标头,例如用户代理、主机或内容长度。

下面是预检请求的示例:

OPTIONS http://myservice.azurewebsites.net/api/test HTTP/1.1
Accept: */*
Origin: http://myclient.azurewebsites.net
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept, x-my-custom-header
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: myservice.azurewebsites.net
Content-Length: 0

外部测试前请求使用 HTTP OPTIONS 方法。 它包括两个特殊标头:

  • Access-Control-Request-Method:将用于实际请求的 HTTP 方法。
  • Access-Control-Request-Headers:应用程序在实际请求上设置的请求标头列表。 (同样,这不包括浏览器设置的标头。

下面是一个示例响应,假设服务器允许请求:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT
Date: Wed, 05 Jun 2013 06:33:22 GMT

响应包括一个 Access-Control-Allow-Methods 标头,该标头列出了允许的方法,还可以选择一个 Access-Control-Allow-Headers 标头,其中列出了允许的标头。 如果预检请求成功,浏览器会发送实际请求,如前所述。

通常用于使用预检 OPTIONS 请求测试终结点的工具默认情况下不会发送所需的 OPTIONS 标头。 确认 Access-Control-Request-Method 使用请求发送标头和 Access-Control-Request-Headers OPTIONS 标头是否通过 IIS 到达应用。

若要配置 IIS 以允许 ASP.NET 应用接收和处理 OPTION 请求,请将以下配置添加到该部分中应用的 web.config 文件中 <system.webServer><handlers>

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

删除 OPTIONSVerbHandler 阻止 IIS 处理 OPTIONS 请求。 替换 ExtensionlessUrlHandler-Integrated-4.0 允许 OPTIONS 请求访问应用,因为默认模块注册仅允许使用无扩展 URL 的 GET、HEAD、POST 和 DEBUG 请求。

[EnableCors] 的范围规则

可以为应用程序中的所有 Web API 控制器启用每个操作、每个控制器或全局 CORS。

每个操作

若要为单个操作启用 CORS,请对操作方法设置 [EnableCors] 属性。 以下示例仅为 GetItem 该方法启用 CORS。

public class ItemsController : ApiController
{
    public HttpResponseMessage GetAll() { ... }

    [EnableCors(origins: "http://www.example.com", headers: "*", methods: "*")]
    public HttpResponseMessage GetItem(int id) { ... }

    public HttpResponseMessage Post() { ... }
    public HttpResponseMessage PutItem(int id) { ... }
}

每个控制器

如果在控制器类上设置了 [EnableCors] ,则它适用于控制器上的所有操作。 若要禁用操作的 CORS,请将 [DisableCors] 属性添加到该操作。 以下示例为除以下 PutItem各项方法启用 CORS。

[EnableCors(origins: "http://www.example.com", headers: "*", methods: "*")]
public class ItemsController : ApiController
{
    public HttpResponseMessage GetAll() { ... }
    public HttpResponseMessage GetItem(int id) { ... }
    public HttpResponseMessage Post() { ... }

    [DisableCors]
    public HttpResponseMessage PutItem(int id) { ... }
}

全球

若要为应用程序中的所有 Web API 控制器启用 CORS,请将 EnableCorsAttribute 实例传递给 EnableCors 方法:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var cors = new EnableCorsAttribute("www.example.com", "*", "*");
        config.EnableCors(cors);
        // ...
    }
}

如果在多个范围内设置属性,优先级顺序为:

  1. 操作
  2. 控制器
  3. 全局

设置允许的源

[EnableCors] 属性的源参数指定允许哪些源访问资源。 该值是允许的源的逗号分隔列表。

[EnableCors(origins: "http://www.contoso.com,http://www.example.com", 
    headers: "*", methods: "*")]

还可以使用通配符值“*”允许来自任何源的请求。

在允许来自任何源的请求之前,请仔细考虑。 这意味着任何网站都可以对 Web API 进行 AJAX 调用。

// Allow CORS for all origins. (Caution!)
[EnableCors(origins: "*", headers: "*", methods: "*")]

设置允许的 HTTP 方法

[EnableCors] 属性的方法参数指定允许哪些 HTTP 方法访问资源。 若要允许所有方法,请使用通配符值“*”。 以下示例仅允许 GET 和 POST 请求。

[EnableCors(origins: "http://www.example.com", headers: "*", methods: "get,post")]
public class TestController : ApiController
{
    public HttpResponseMessage Get() { ... }
    public HttpResponseMessage Post() { ... }
    public HttpResponseMessage Put() { ... }    
}

设置允许的请求头

本文前面介绍了预检请求如何包括 Access-Control-Request-Headers 标头,其中列出了应用程序设置的 HTTP 标头(所谓的“作者请求标头”)。 [EnableCors] 属性的标头参数指定允许哪些作者请求标头。 若要允许任何标头,请将标头设置为“*”。 若要允许特定标头,请将标头设置为允许标头的逗号分隔列表:

[EnableCors(origins: "http://example.com", 
    headers: "accept,content-type,origin,x-my-header", methods: "*")]

但是,浏览器在设置 Access-Control-Request-Header 的方式中并不完全一致。 例如,Chrome 当前包括“origin”。 FireFox 不包含标准标头(如“Accept”),即使应用程序在脚本中设置它们也是如此。

如果将标头设置为“*”以外的任何内容,则应至少包括“accept”、“content-type”和“origin”,以及要支持的任何自定义标头。

设置允许的响应标头

默认情况下,浏览器不会向应用程序公开所有响应标头。 默认情况下可用的响应头包括:

  • Cache-Control
  • Content-Language
  • Content-Type
  • 到期
  • Last-Modified
  • Pragma

CORS 规范调用这些 简单的响应标头。 若要使其他标头可用于应用程序,请设置 [EnableCors]exposedHeaders 参数。

在以下示例中,控制器的方法设置名为“X-Custom-Header”的 Get 自定义标头。 默认情况下,浏览器不会在跨源请求中公开此标头。 若要使标头可用,请在公开的Headers 中包含“X-Custom-Header”。

[EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "X-Custom-Header")]
public class TestController : ApiController
{
    public HttpResponseMessage Get()
    {
        var resp = new HttpResponseMessage()
        {
            Content = new StringContent("GET: Test message")
        };
        resp.Headers.Add("X-Custom-Header", "hello");
        return resp;
    }
}

在跨域请求中传递凭据

凭据需要在 CORS 请求中进行特殊处理。 默认情况下,浏览器不会发送具有跨源请求的任何凭据。 凭据包括 Cookie 以及 HTTP 身份验证方案。 若要使用跨域请求发送凭据,客户端必须将 XMLHttpRequest.withCredentials 设置为 true。

直接使用 XMLHttpRequest

var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;

在 jQuery 中:

$.ajax({
    type: 'get',
    url: 'http://www.example.com/api/test',
    xhrFields: {
        withCredentials: true
    }

此外,服务器必须允许凭据。 若要允许 Web API 中的跨域凭据,请将 [EnableCors] 属性上的 SupportsCredentials 属性设置为 true:

[EnableCors(origins: "http://myclient.azurewebsites.net", headers: "*", 
    methods: "*", SupportsCredentials = true)]

如果此属性为 true,HTTP 响应将包含 Access-Control-Allow-Credentials 标头。 此标头告知浏览器服务器允许跨源请求的凭据。

如果浏览器发送凭据,但响应不包括有效的 Access-Control-Allow-Credentials 标头,则浏览器不会向应用程序公开响应,并且 AJAX 请求失败。

请注意将 SupportsCredentials 设置为 true,因为这意味着另一个域上的网站可以代表用户将登录用户的凭据发送到 Web API,而不会让用户知道。 CORS 规范还表示,如果 SupportsCredentials 为 true,则将源设置为“*”无效。

自定义 CORS 策略提供程序

[EnableCors] 属性实现 ICorsPolicyProvider 接口。 可以通过创建派生自 Attribute 并实现 ICorsPolicyProvider类来提供自己的实现。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class MyCorsPolicyAttribute : Attribute, ICorsPolicyProvider 
{
    private CorsPolicy _policy;

    public MyCorsPolicyAttribute()
    {
        // Create a CORS policy.
        _policy = new CorsPolicy
        {
            AllowAnyMethod = true,
            AllowAnyHeader = true
        };

        // Add allowed origins.
        _policy.Origins.Add("http://myclient.azurewebsites.net");
        _policy.Origins.Add("http://www.contoso.com");
    }

    public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request)
    {
        return Task.FromResult(_policy);
    }
}

现在,可以应用将放置 [EnableCors] 的任何位置的属性。

[MyCorsPolicy]
public class TestController : ApiController
{
    .. //

例如,自定义 CORS 策略提供程序可以从配置文件中读取设置。

作为使用属性的替代方法,可以注册创建 ICorsPolicyProvider 对象的 ICorsPolicyProviderFactory 对象。

public class CorsPolicyFactory : ICorsPolicyProviderFactory
{
    ICorsPolicyProvider _provider = new MyCorsPolicyProvider();

    public ICorsPolicyProvider GetCorsPolicyProvider(HttpRequestMessage request)
    {
        return _provider;
    }
}

若要设置 ICorsPolicyProviderFactory,请在启动时调用 SetCorsPolicyProviderFactory 扩展方法,如下所示:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.SetCorsPolicyProviderFactory(new CorsPolicyFactory());
        config.EnableCors();

        // ...
    }
}

浏览器支持

Web API CORS 包是服务器端技术。 用户的浏览器还需要支持 CORS。 幸运的是,所有主要浏览器的当前版本都包括 对 CORS 的支持。