在 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”,用于托管 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 应用服务 Web 应用.) 若要验证 Web API 是否正常工作,请导航到 http://hostname/api/test/,其中主机名是在其中部署应用程序的域。 应会看到响应文本“GET:测试消息”。

    显示测试消息的 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 应用或将其发布到其他网站。

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

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

注意

如果在 Fiddler 等工具中watch 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...
    }
}

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

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

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

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

CORS 的工作原理

本部分介绍 CORS 请求在 HTTP 消息级别上发生的情况。 了解 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 规范调用这些“作者请求标头”。) 规则不适用于 浏览器 可以设置的标头,例如用户代理、主机或 Content-Length。

下面是预检请求的示例:

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 请求的终结点的工具 (例如 FiddlerPostman) 默认情况下不会发送所需的 OPTIONS 标头。 确认 Access-Control-Request-MethodAccess-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] 属性。 以下示例仅为 方法启用 CORS GetItem

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] 属性的 origins 参数指定允许哪些源访问资源。 该值是允许的来源的逗号分隔列表。

[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] 属性的 methods 参数指定允许哪些 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 标头,其中列出了应用程序 (所谓的“author request headers”) 设置的 HTTP 标头。 [EnableCors] 属性的 headers 参数指定允许哪些作者请求标头。 若要允许任何标头,请将 标头 设置为“*”。 若要允许特定标头,请将 标头 设置为允许的标头的逗号分隔列表:

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

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

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

设置允许的响应标头

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

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

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

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