在 ASP.NET Web API 2 中啟用跨原始來源要求

作者:Mike Wasson

此內容適用於 .NET 的早期版本。 新開發應該使用 ASP.NET Core。 有關在 ASP.NET Core 中使用 Web API 和跨原始來源請求 (CORS) 的詳細資訊,請參閱:

瀏覽器安全性可防止網頁對另一個網域提出 AJAX 要求。 這項限制也稱為同源原則,可防止惡意網站從另一個網站讀取敏感性資料。 不過,有時候您可能會想要讓其他網站呼叫您的 Web API。

跨原始來源資源共用 (CORS) 是 W3C 標準,可讓伺服器放寬 same-origin 原則。 使用 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 的來源與以上兩個不同:

  • 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. New ASP.NET Web Application 對話方塊中,選取 Empty 專案範本。 在 [新增資料夾和核心參照 底下,選取 [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 Apps。) 若要確認 Web API 是否正常運作,請瀏覽至 http://hostname/api/test/,其中 hostname 是您部署應用程式的網域。 您應該會看到回應文字「GET:測試訊息」。

    顯示測試訊息的網頁瀏覽器

建立 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 應用程式,或將其發佈至另一個網站。

當您按一下 [試用] 按鈕時,AJAX 要求會使用下拉式方塊中所列的 HTTP 方法提交至 WebService 應用程式 (GET、POST 或 PUT)。 這可讓您檢查不同的跨原始來源要求。 目前,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...
    }
}

針對 [origins] 參數,請使用您部署 WebClient 應用程式的 URI。 這允許來自 WebClient 的跨原始來源要求,同時仍然不允許所有其他跨網域要求。 稍後,我會更詳細地描述 [EnableCors] 的參數。

請勿在origins URL 的結尾包含正斜線。

重新部署更新的 WebService 應用程式。 您不需要更新 WebClient。 現在 WebClient 的 AJAX 要求應該會成功。 GET、PUT 和 POST 方法皆可允許。

顯示成功測試訊息的網頁瀏覽器

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 標頭 (if set) 是下列其中一項:

    • 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 標頭,其中會列出允許的標頭。 如果預檢要求成功,瀏覽器會傳送實際的要求,如先前所述。

通常用來測試具有預檢 OPTIONS 要求之端點的工具預設不會傳送必要的 OPTIONS 標頭。 確認 Access-Control-Request-MethodAccess-Control-Request-Headers 標頭會隨要求一起傳送,而且 OPTIONS 標頭會透過 IIS 傳送到應用程式。

若要將 IIS 設定為允許 ASP.NET 應用程式接收及處理 OPTION 要求,請將下列設定新增至應用程式的 <system.webServer><handlers> 區段中的 web.config 檔案:

<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 要求抵達應用程式,因為預設模組註冊只允許 GET、HEAD、POST 和 DEBUG 要求與無擴充 URL。

[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] 屬性新增至動作。 下列範例會為每個方法啟用 CORS,但 PutItem 除外。

[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 標頭,列出應用程式所設定的 HTTP 標頭 (所謂的「作者要求標頭」)。 [EnableCors] 屬性的 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
  • 內容-類型
  • 到期
  • 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 的類別並實作 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 的支援