在ASP.NET Web API2中启用Cross-Origin请求

浏览器安全性可防止网页向其他域发送AJAX请求。此限制称为同源策略same-origin policy,并防止恶意站点从其他站点读取发送数据。但是,有时您可能希望让其他网站调用您的Web API。

跨域资源共享Cross-Origin Resource Sharing 即 CORS)是一种W3C标准,允许服务器放松同源策略。使用CORS,服务器可以明确允许一些跨域请求,同时拒绝其他请求。CORS比诸如JSONP之类的技术更安全,更灵活。本教程将介绍如何在Web API应用程序中启用CORS。

本教程中使用的软件版本

介绍

本教程演示了ASP.NET Web API中的CORS支持。我们将首先创建两个ASP.NET项目,一个称为“WebService”,它承载一个Web API控制器,另一个称为“WebClient”,它调用WebService。因为这两个应用程序托管在不同的域,所以从WebClient到WebService的AJAX请求是一个跨域请求。

 

什么是“同源”?

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

这两个URL的起源相同:

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

这些网址的来源与前两个不同:

  • http://example.net - 不同的域名
  • http://example.com:9000/foo.html - 不同的端口
  • https://example.com/foo.html - 不同的方案(Http 以及 Https
  • http://www.example.com/foo.html - 不同的子域名
注意:比较来源时,Internet Explorer不考虑端口。

创建WebService项目

启动Visual Studio并创建一个新的ASP.NET Web应用程序项目。选择项目模板。在“为以下项添加文件夹和核心引用”下,选择Web API复选框。(可选)选择“云端主机”选项将应用程序部署到Mircosoft Azure。Microsoft在免费的Azure试用版帐户中为多达10个网站提供免费的虚拟主机。

 

添加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")
            };
        }
    }
}

您可以在本地运行应用程序或部署到Azure。(对于本教程中的截图,我部署到Azure App Service Web Apps。)要验证Web API是否正常工作,请导航到http://hostname/api/test/,其中hostname是您部署应用程序的域。您应该看到响应文本“GET:Test Message”。

 

创建WebClient项目

创建另一个ASP.NET Web应用程序项目并选择MVC项目模板。或者,选择更改身份验证 > 不进行身份验证。您不需要本教程的身份验证。

 

在解决方案资源管理器中,打开文件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: 用你的WebService URL进行替换
    var serviceUrl = 'http://mywebservice/api/test'; 

    function sendRequest() {
        var method = $('#method').val();

        $.ajax({
            type: method,
            url: serviceUrl
        }).done(function (data) {
            $('#value1').text(data);
        }).error(function (jqXHR, textStatus, errorThrown) {
            $('#value1').text(jqXHR.responseText || textStatus);
        });
    }
</script>
}

对于serviceUrl变量,使用已发布的WebService应用程序的URI。现在在本地运行WebClient应用程序或将其发布到另一个网站。

点击“Try it”按钮,使用下拉框(GET,POST或PUT)中列出的HTTP方法向WebService应用程序提交AJAX请求。这样我们可以检查不同的跨域请求。现在,WebService应用程序不支持CORS,因此如果您单击按钮,您将收到错误。

 

注意

如果您在像Fiddler这样的工具中观看HTTP流量,您将看到浏览器确实发送了GET请求,并且请求成功,但AJAX调用返回错误。重要的是要明白同源策略不会阻止浏览器发送请求。相反,它阻止应用程序看到响应

 

启用CORS

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

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方法都是允许的.

 

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请求失败。具体来说,浏览器不允许请求。即使服务器返回成功的响应,浏览器也不会使响应可用于客户端应用程序。

预检请求(Preflight Requests)

对于某些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规范调用这些“作者请求头”。)该规则不适用于浏览器可以设置的标头,例如User-Agent,Host或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标头,其中列出了允许的标头。如果预检请求成功,则浏览器发送实际请求,如前所述。

[EnableCors]的作用域

您可以为应用程序中的每个Action、每个Controller以及全局启用CORS。

Action:

要为单个Action启用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) { ... }
}

Controller:

如果在控制器类上设置了[EnableCors],它将适用于控制器上的所有Action。要禁用Action上的的CORS,请将[DisableCors]属性添加到Action上。以下示例为除了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. Action
  2. Controller
  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() { ... }    
}

设置允许的请求头

之前我描述了预检请求(preflight requests)如何可能包括Access-Control-Request-Headers头,列出了应用程序设置的HTTP头(所谓的“作者请求头”)。[EnableCors]的heads属性指定哪个作者请求头(author request headers)是被允许的。要允许所有标头,将标头设置为“*”。要将特定标头列入白名单,请将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,则origins 参数设置为“*”是无效的。

自定义CORS策略提供程序

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

[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

Internet Explorer 8和Internet Explorer 9对CORS有部分支持,使用旧版XDomainRequest对象而不是XMLHttpRequest。有关详细信息,请参阅XDomainRequest - 限制,限制和解决方法

posted @ 2017-05-03 18:48  X-Cracker  阅读(1075)  评论(0编辑  收藏  举报