[Web API] Web API CORS Ajax 跨網域存取

說明 Web API 發生跨網域問題時該如何用 CORS 處理,以 WebApiContrib 跟 Thinktecture.IdentityModel 使用為範例。

前言


  在依照第一篇 進擊的 Web API : 建立 Web API 專案 完成後,當然會想要在現有的專案使用此 Web API,但是當從另一個網站發送 Ajax 要求呼叫 Web API 時卻發生了錯誤,先利用 Firefox 開發者工具查看,如下。

 

 

  由上圖可以發現 Status Code 200 OK 表示此 Request 應該是發送成功的,但是為何 Ajax 請求回覆會被導向到 error 事件中呢? 在看了回應的內容竟然也是空的,實際查了些資料發現 Web API 預設尚未支援跨網域呼叫,什麼是跨網域呼叫? 舉例說明,先看以下兩個網址:

 

http://localhost:57944/Web_02/Default.aspx
http://localhost:57979/api/products

 

  區分是否為跨網域是依照網站的 Domain 與 Port 做區分,當兩個網頁執行在相同的 Domain 與 Port 下時就代表的是同網域,而如兩個網頁在不同的 Domain 與 Port 或同 Domain 不同 Port 時就會變成跨網域,所以上方兩個網址就代表了是跨網域的情況,在這種情況下因為安全性問題所以發送 Ajax 要求時將被拒絕,這時就必須透過使用 CORS 或 JSONP 方式存取。

 

  CORS (Cross-origin resource sharing) 跨來源資源共享,是針對如用戶端透過 Ajax 從 A 網域呼叫 B 網域服務的互動而定義的標準,也屬於 W3C 中推廣的標準之一,簡單的說就是使用 Ajax 跨網域呼叫的一種標準,要使用跨網域的方法時其實還有另一種選擇「JSONP」,JSONP 的作法是一種透過 Server 回傳資料與 Callback 方法名後 Client 再去呼叫 Callback 方法接收其值,相較於 CORS 來說,JSONP 只提供 HTTP GET 動詞可以使用,而 CORS 提供了所有 HTTP 動詞且安全性比較高。

 

  如要使用 CORS 的方法主要是透過在 HTTP Header 中加入 Access-Control-Allow-Origin 此回應標頭來讓用戶端檢查,當回應標頭含有 Access-Control-Allow-Origin 時資料將正常顯示,如未包含時雖然呼叫成功但是資料不會顯示出來 (另可參考 ASP.NET WEB API CORS預覽功能完整剖析 )。

 

Web API 使用 CORS


  參考 ASP.NET WEB API CORS預覽功能完整剖析 中提到需要安裝 CORS 組件,不過我在 VS 2010 中並無法尋找到此套件,所以推測可能是需要 .Net 4.5 與 VS 2012 才行,所以在尋找替代方案時發現了另兩個解決方案,參考 Implementing CORS support in ASP.NET Web APIsImplementing CORS support in ASP.NET Web APIs – take 2CORS support in ASP.NET Web API – RC version ,文章中提供了 Implementing CORS support in ASP.NET Web APIs (per action basis)  範例並且說明了如何在 Web API 使用 CORS,另外此 CORS 也已被包含在 WebApiContrib 集成套件中,而另一個是使用 Thinktecture.IdentityModel 套件,此套件也不錯,可以自行配置允許的網域、路由與存取的 HTTP 動詞等條件,也是使用的選擇之一。

 

使用 WebApiContrib 套件實作 CORS

  讓我們開始使用 CORS,首先在 VS 2010 上使用 NuGet 搜尋 WebApiContrib 並加入 Web API 專案中。

 

  開啟 ProductsController 載入以下命名空間

using WebApiContrib.Filters;
using WebApiContrib.Selectors;

  現在我們可以針對要啟用允許使用 CORS 的方法加入 EnableCors 屬性,方法很簡單只要在允許啟用的方法上加入 EnableCors 屬性即可,如下

[EnableCors]
public IEnumerable<Product> GetAllProducts()
{
    return new ProductDao().GetProducts();
}

 

  在 Cors 設定好之後,接者在此方案中建立了一個新網站用來做為不同網域下呼叫 Ajax 要求測試用,使用 jQuery Ajax 的方式撰寫 Script,如下

$(function () {
    $("#btnGetAllProducts").click(function () {
        $.ajax({
            url: 'http://localhost:57979/api/products/',
            type: 'GET',
            dataType: 'json',
            cache: false,
            error: function (xhr) {
                alert('Ajax request error!');
            },
            success: function (data) {
                $.each(data, function (key, val) {
                    var str = val.Name + ': $' + val.Price;
                    $('<li/>', { text: str }).appendTo($('#result'));
                });
            }
        });
    });
});
<div>
    <h1>Web API</h1>
    <h2>GetAllProducts</h2>
    <input id="btnGetAllProducts" type="button" value="GetAllProducts" /><br />
    <ul id="result">
    </ul>
</div>

 

  重新測試一次 Ajax 呼叫結果如下

 

  發現返回的 JSON 的資料已經確實顯示了,再來看一下 Header 的內容。

 

  從標頭資訊中可以看到當請求用戶端發送查詢要求時請求標頭含 Origin Header 指著來源網頁,而回應標頭則帶入 Access-Control-Allow-Origin Header,經過用戶端驗證含 Access-Control-Allow-Origin Header 後才能正常的顯示資料。

 

  在我們使用此方法時加入 EnableCors 屬性是代表不管誰呼叫此方法都允許顯示資料給對方,但是基於安全性考量時有些方法可能只想提供給特定的對象存取,這時我們就可以自訂一個繼承 ActionFilterAttribute 並複寫 OnActionExecuted 方法的類別,如下

public class CustomerEnableCorsAttribute : ActionFilterAttribute
{
    const string Origin = "Origin";
    const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
    List<string> passOrigin = new List<string>();

    public CustomerEnableCorsAttribute() { }
    public CustomerEnableCorsAttribute (params string[] pPassOrigin)
    {
        foreach (string url in pPassOrigin)
        {
            passOrigin.Add(url);
        }
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        if (actionExecutedContext.Request.Headers.Contains(Origin))
        {
            string originHeader = 
                actionExecutedContext.Request.Headers.GetValues(Origin).FirstOrDefault();
            if (!string.IsNullOrEmpty(originHeader))
            {
                if (passOrigin.Count != 0)
                {
                    foreach (string url in passOrigin)
                    {
                        Uri allow = new Uri(url);
                        Uri check = new Uri(originHeader);
                        if (Uri.Compare(allow, check, 
                                        UriComponents.HostAndPort, 
                                        UriFormat.SafeUnescaped, 
                                        StringComparison.CurrentCulture) == 0)
                            actionExecutedContext.Response.Headers.Add(
                                                 AccessControlAllowOrigin, originHeader);
                    }
                }   
                else
                    actionExecutedContext.Response.Headers.Add(
                                                 AccessControlAllowOrigin, originHeader);
            }
        }
    }
}

  以上是一個自訂判斷來源網域是否符合允許條件的範例,接著一樣於方法上加入屬性並且多傳入允許的網域位置即可,如下

[CustomerEnableCors ("http://localhost:19594")]
public IEnumerable<Product> GetAllProducts()
{
    return new ProductDao().GetProducts();
}

 

使用 Thinktecture.IdentityModel 套件實作 CORS

  接下來也稍微講解使用 Thinktecture.IdentityModel 此套件該如何處理,首先一樣使用 NuGet 下載該套件。

 

  安裝完成後在 Web API 專案 App_Start 資料夾下建立一個 CorsConfig 類別,如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using Thinktecture.IdentityModel.Http.Cors.WebApi;

namespace TWebApi_01
{
    public class CorsConfig
    {
        public static void RegisterCors(HttpConfiguration httpConfig)
        {
            WebApiCorsConfiguration corsConfig = new WebApiCorsConfiguration();
            corsConfig.RegisterGlobal(httpConfig);

            corsConfig
                 .ForResources("products")
                 .ForOrigins("http://localhost:19594")
                 .AllowMethods("GET", "POST", "PUT", "DELETE");
        }
    }
}

  看到 corsConfig.ForRes..... 此處,這裡就是在設定能夠存取的權限範圍

  • ForResources(string[] 方法是指定哪個 Controller
  • ForOrigins 方法是指定允許存取的網域
  • AllowMethods 方法是指定允許存取的 HTTP 動詞

 

  接著我們需要註冊 CorsConfig 設定,開啟 Global 類別後於 Application_Start() 方法中加入註冊代碼

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    //註冊 CorsConfig
    CorsConfig.RegisterCors(GlobalConfiguration.Configuration);
}

  兩動作完成後就可以了,如需要知道詳細設置可以參考官方文件,以上內容就是針對於 Web API 跨網域問題使用 CORS 處理的說明。

 

範例程式碼


TWebApi_02.part01.rar

TWebApi_02.part02.rar

TWebApi_02.part03.rar

TWebApi_02.part04.rar

話說....這專案檔還真大... Orz

參考資料


ActionFilterAttribute 類別

ASP.NET WEB API CORS預覽功能完整剖析

Implementing CORS support in ASP.NET Web APIs

Implementing CORS support in ASP.NET Web APIs – take 2

CORS support in ASP.NET Web API – RC version

 

 


以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)