Cross-origin resource sharing(CORS)
Cross-origin resource sharing (跨來源資源分享,以下稱CORS)是W3C 提案的技術標準,定義伺服器與用瀏覽器在跨來源(即cross domain,跨網域)呼叫時的互動方式。CORS標準允許網頁發出跨網域的AJAX請求,讓實作了同源策略(same-origin)的瀏覽器在安全條件下去呼叫進行有限制的跨網域AJAX請求。
CORS透過設定HTTP Header(標頭)設置那些網域的網站可以跨網域存取,目前W3C定義九個Response Header(回應標頭)可以設置:
Response Header | 說明 |
---|---|
Access-Control-Allow-Origin | 指示是否基於共享資源透過回傳來源請求標頭、“*”或“null”在回應裡。 |
Access-Control-Allow-Credentials | 指示當省略憑證標誌(Credentials flag)未設置時,是否公開請求的回應。當檢查請求(preflight request)的回應一部分,它表明實際的請求可以包含用戶端憑證。 |
Access-Control-Expose-Headers | 指示那些標頭是安全公開在CORS API規範的API。 |
Access-Control-Max-Age | 指示檢查請求可以快取多久時間在快取結果裡。 |
Access-Control-Allow-Methods | 指示實際請求期間,可以使用那些方法做為請求回應的一部分。 |
Access-Control-Allow-Headers | 指示實際請求期間,可以使用那些標頭名稱做為請求回應的一部分。 |
Origin | 指示跨來源請求或檢查請求來自於。 |
Access-Control-Request-Method | 指示那種方法在實際請求中做為檢查請求的一部分。 |
Access-Control-Request-Headers | 指示那種標頭在實際請求中做為檢查請求的一部分。 |
例如,我們可以設置以下CORS標頭:
Access-Control-Allow-Origin: http://kkbruce.net Access-Control-Allow-Methods: PUT, DELETE
這表示,除了網站本身以外,還允許kkbruce.net網域進行跨網域的AJAX請求且只允許PUT和DELETE方法的請求。允許的網域可以設定多組,也能設定為“*”代表不設限。
ASP.NET Web API的CORS支援
要讓ASP.NET Web API的專案支援CORS功能,目前必須透過增加Signed nightly builds(每日建構)來取得Preview版本的CORS組件。
- ASP.NET Web API的CORS組件是2013/4/5才發佈Preview,Preview的設置步驟與未來Release可能有所不同。
- 目前Preview的CORS套件無法在非英文專案上測試,請切換Visual Studio 2012語言至English(需額外安裝語言檔),在英文介面下新增Web API的專案,這樣在安裝CORS套件(步驟三)時才不會出錯。例如,中文介面的Visual Studio建立的Web API專案上安裝Preview版的CORS套件會出現以下錯誤訊息:
至於這個 Microsoft.AspNet.WebApi.Core.zh-Hant 無法安裝的問題,算不算 Bug ,我個人認為是不算,這比較像 NuGet 相依性處理有問題。反正記得,你要測試一些 Preview 功能,又碰到語系元件的問題,我個人通常是切換到英文語系後都能解決。
Web API CORS功能建置步驟
- 設定Signed Nightly Builds
開啟Visual Studio 2012 工具 → 選項 → 套件管理員 → 套件來源,新增一筆套件來源,來源位址為:http://www.myget.org/F/aspnetwebstacknightly/。
- 移除Microsoft.AspNet.Mvc.FixedDisplayModes套件
在開啟或新增的Web API專案中(需.NET Framework 4.5且由英文版Visual Studio 2012建立的英文專案),開啟NuGet套件管理員,找到Microsoft.AspNet.Mvc.FixedDisplayModes套件並移除它。
PM> Uninstall-Package Microsoft.AspNet.Mvc.FixedDisplayModes
- 從nightly builds安裝Microsoft.AspNet.WebApi.Cors套件
從nightly builds裡可以搜尋到Microsoft.AspNet.WebApi.Cors套件並進行安裝。注意,此步驟目前無法使用指令模式來安裝CORS套件。
進行下一步驟之前,先建置與啟動網站,將看到ASP.NET MVC的錯誤訊息:
- 修正web.config的binding redirects組態
上述錯誤是因為CORS組件有更新一些DLL造成,需要將web.config裡的
<assemblyBinding>
段落使用以下內容替換:<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:bcl="urn:schemas-microsoft-com:bcl"> <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages.Razor" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-5.0.0.0" newVersion="5.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="EntityFramework" publicKeyToken="b77a5c561934e089" /> <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> </dependentAssembly> </assemblyBinding>
重新建置與啟動網站,可以正常看到ASP.NET MVC的首頁。
Web API CORS跨網域服務存取測試
現在請讀者另外建置一個ASP.NET Web API專案,透過Help Page的Test API當請求端(請參考《ASP.NET MVC4網站開發美學》二刷中Help Page章節建置Test API),由於兩個專案的Port不一樣,會當成跨網域的AJAX請求,預請情況下,CORS是不啟用的。
現在筆者專案配置如下:
- Test Client:http://localhost:13466/
- Web API:http://localhost:13107/
在Test Client部分,請不要使用IE進行測試,IE辨識得出來以上兩者是在進行內部測試,這部分在書中JSONP章節說明過。另外一種測試方式,你可以先將Test Client部署至非本機的IIS或Windows Azure上以模擬跨網域的情境。不過,也不需要太複雜,換個非IE瀏覽器即可。
由Test Client向Web API發出的請求因為Port號的不同被認定為跨網域的請求,我們可以開啟開發者工具查詢錯誤訊息:
關鍵訊息「XMLHttpRequest cannot load http://localhost:13107/api/Values. Origin http://localhost:13466 is not allowed by Access-Control-Allow-Origin.」已經說明Test API來源http://localhost:13466沒有得到Access-Control-Allow-Origin的允許,會得到一個Status Code 200但錯誤的訊息。
啟用Web API的CORS支援
要啟用Web API裡的CORS功能支援,最簡單的方法,只需要在WebApiConfig組態進行啟用。
- 在WebApiConfig組態設置必要命名空間
Imports System.Web.Http Imports System.Web.Http.Cors
- 設置EnableCors方法
Public Class WebApiConfig Public Shared Sub Register(ByVal config As HttpConfiguration) config.Routes.MapHttpRoute( _ name:="DefaultApi", _ routeTemplate:="api/{controller}/{id}", _ defaults:=New With {.id = RouteParameter.Optional} _ ) config.EnableSystemDiagnosticsTracing() config.EnableCors(New EnableCorsAttribute()) End Sub End Class
重新建置專案及啟動網站,再一次透過Test API進行跨網域測試。
現在順利透過CORS進行AJAX跨網域存取。我們透過開發者工具來看一下Headers瞭解發生了什麼事。
- Part 1
Request URL:http://localhost:13107/api/Values Request Method:GET Status Code:200 OK
這是由Test API(localhost:13466)向Web API(localhost:13107)送出的請求與回應,我們得到Status Code 200表示成功。
- Part 2 Request Headers
Accept:*/* Accept-Charset:Big5,utf-8;q=0.7,*;q=0.3 Accept-Encoding:gzip,deflate,sdch Accept-Language:zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4 Connection:keep-alive Host:localhost:13107 Origin:http://localhost:13466 Referer:http://localhost:13466/Help/Api/GET-api-Values User-Agent:Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31
在請求標頭中會由Origin告知來源,即「我是誰,我從那裡來」。
- Part 3 Response Headers
HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; charset=utf-8 Expires: -1 Server: Microsoft-IIS/8.0 Access-Control-Allow-Origin: * X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?...?= X-Powered-By: ASP.NET Date: Thu, 18 Apr 2013 06:59:13 GMT Content-Length: 19
Access-Control-Allow-Origin標頭進行可跨網域AJAX存取設置。“*”代表不設限。
Web API的CORS作用範圍
nightly builds下載的CORS組件會新增兩個新組件。
- System.Web.Cors.dll
此組件包含CORS核心函式庫且不需要依賴System.Web.dll或System.Web.Http.dll。 - System.Web.Http.Cors.dll
此組件包含啟用與設置Web API的CORS函式庫且不需要依賴System.Web.Cors.dll和System.Web.Http.dll。
CORS組件會新增HttpConfiguration的擴充方法來設置CORS的功能,此設置可以進行Web API的全域設置或是以Controller或Action為單位來設置。
全域設置
進行全域設置只需要在WebApiConfig組態中進行EnableCors方法的呼叫,例如,以下設置會允許CORS所有來源(Origins)、方法(Methods)與標題(Headers)。
Imports System.Web.Http.Cors Public Class WebApiConfig Public Shared Sub Register(ByVal config As HttpConfiguration) ' 其他省略 config.EnableCors(New EnableCorsAttribute()) End Sub End Class
參數EnableCorsAttribute屬性稍後說明。
Controller為單位設置
我們可以以Controller為單位進行CORS設置,這非常方便開發者進行安全性管理,只提供開放Controller讓外部存取。例如,經過DTO處理的Controller來提供給外部使用,可避免外部去接觸到不必要的資訊。要以Controller為單位進行CORS設置,有二個步驟。
- 取消全域設置裡的EnableCorsAttribute屬性
Imports System.Web.Http.Cors Public Class WebApiConfig Public Shared Sub Register(ByVal config As HttpConfiguration) ' 其他省略 config.EnableCors() End Sub End Class
- 在Controller中設置EnableCors屬性
Imports System.Web.Http.Cors <EnableCors> Public Class ValuesController Inherits ApiController ' 內容省略 End Class
Action為單位設置
與Controller為單位設置類似,Action為單位的設置是最細膩的設置選項,可控制只有某些Action可以來進行CORS的AJAX請求。
- 取消全域設置裡的EnableCorsAttribute屬性
Imports System.Web.Http.Cors Public Class WebApiConfig Public Shared Sub Register(ByVal config As HttpConfiguration) ' 其他省略 config.EnableCors() End Sub End Class
- 在Action中設置EnableCors屬性
Imports System.Web.Http.Cors Public Class ValuesController Inherits ApiController ' 其他省略 <EnableCors> Public Function GetValue(ByVal id As Integer) As String Return "value" End Function End Class
CORS屬性運作範圍
如果將EnableCorsAttribute屬性設置至全部範圍(Global、Controller、Action),預設定義以由內而外執行:
- Action
- Controller
- Global
禁用CORS屬性
有允許的EnableCors屬性就有不允許的DisableCors屬性,讓我們可以設置例外排除,以下我們對Controller進行EnableCore屬性的設置,但在此Controller中又有某些Action不允許CORS的操作。
Imports System.Web.Http.Cors <EnableCors> Public Class ValuesController Inherits ApiController ' 其他省略 <DisableCors> Public Sub DeleteValue(ByVal id As Integer) End Sub End Class
一般性的方法允許CORS的AJAX請求,但如HTTP DELETE等方法並不希望讓本身網站以外進行操作,在考慮到安全性時,此種Action裡的DisableCors屬性的設置特別有用。
EnableCorsAttribute屬性
EnableCorsAttribute屬性在System.Web.Http.Cors命名空間,依W3C的CORS標準 實作了以下屬性:
- Origins
定義:Public Property Origins As String()
範例:New String() {"http://localhost, http://kkbruce.net"}
- Headers
定義:Public Property Headers As String()
範例:New String() {"Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma"}
- Methods
定義:Public Property Methods As String()
範例:New String() {"GET, HEAD, POST"}
- ExposedHeaders
定義:Public Property ExposedHeaders As String()
- SupportsCredentials
定義:Public Property SupportsCredentials As Boolean
使用者憑證是指Cookie、HTTP身分驗證和用戶端SSL憑證等。
- PreflightMaxAge
定義:Public Property PreflightMaxAge As Long
String()代表著可以傳入字串陣列。
預設EnableCorsAttribute屬性允許所有Origins、Methods、Headers的跨網域AJAX請求。當你指定Origins時,就會限制來源,只允許所設置的來源可以進行跨網域AJAX請求。Methods與Headers的設置同理。
以下範例,只允許“http://localhost:13466”和“http://kkbruce.net”對ValuesController進行跨網域AJAX請求。需要注意的是,如果沒有特別指定Methods與Headers屬性的話,預設會對Origins設置的網域開放全部Methods與Headers的請求。
Imports System.Web.Http.Cors <EnableCors(Origins:=New String() {"http://localhost:13466", "http://kkbruce.net"})> Public Class ValuesController Inherits ApiController ' 其他省略 End Class
現在只有Origins裡指定的網域可以對Web API進行跨網域AJAX請求。現在我們修改Controller上的屬性,增加Methods屬性設置:
<EnableCors(Origins:=New String() {" http://localhost:13466"}, Methods:=New String() {"DELETE"})>
建置重新測試HTTP GET、POST、PUT、DELETE四個動詞,基本上應該只有DELETE動詞會有作用,但注意,只要符合Origins屬性的網域預設都會開放HTTP GET動詞,以上設置而言,http://localhost:13466能執行HTTP GET與HTTP DELETE兩個動詞。
以下使用HTTP PUT對”/api/Values/1”進行請求,在Chrome會得到一個NETWORK_ERR的錯誤,查詢Headers頁籤得到幾項訊息:
Status Code:400 Bad Request
在Request Headers裡多出兩筆Headers:
Access-Control-Request-Headers:origin, content-type Access-Control-Request-Method:PUT
使用HTTP DELETE對”/api/Values/1”進行請求則一切正常。
ICorsPolicyProvider介面
System.Web.Http.Cors命名空間提供了兩個重要的介面,分別是ICorsPolicyProvider介面與ICorsPolicyProviderFactory介面,以下進行此兩個介面的討論。
們可以實作ICorsPolicyProvider介面去動態載入其他來源的CORS設置或政策,例如,從web.config或資料庫等。事實上,EnableCorsAttribute屬性與DisableCorsAttribute屬性也是實作此介面,我們可以查詢物件瀏覽器來確認或由反映工具查詢更為清楚。
Public NotInheritable Class EnableCorsAttribute Inherits Attribute Implements ICorsPolicyProvider ' 其他省略 End Class
ICorsPolicyProvider介面只定義了一個GetCorsPolicyAsync方法,此方法是一個非同步(Async)方法。
Public Interface ICorsPolicyProvider Function GetCorsPolicyAsync(ByVal request As HttpRequestMessage) As Task(Of CorsPolicy) End Interface
以下實作一個ICorsPolicyProvider介面的屬性,此屬性由web.config來讀取相關CORS設置。
實作ICorsPolicyProvider介面
- 新增EnableCorsAppSettingsAttribute屬性並實作ICorsPolicyProvider介面
Imports System.Web.Http.Cors Imports System.Web.Cors Imports System.Web.Configuration Imports System.Threading.Tasks <AttributeUsage(AttributeTargets.Method Or AttributeTargets.Class, AllowMultiple:=False)> Public Class EnableCorsAppSettingsAttribute Inherits Attribute Implements ICorsPolicyProvider ' CorsPolicy 由 System.Web.Cors 命名空間提供 Private _policy As CorsPolicy ''' <summary> ''' 1. 進行CorsPolicy物件初始化 ''' 2. 由AppSettings載入origins設置 ''' </summary> ''' <param name="Origins"></param> Public Sub New(Origins As String) _policy = New CorsPolicy() With { .AllowAnyMethod = True, .AllowAnyHeader = True} ' 從web.config的AppSettings載入origins設置 Dim originsString As String = _ WebConfigurationManager.AppSettings(Origins) If Not String.IsNullOrEmpty(originsString) Then For Each origin In originsString.Split(",") _policy.Origins.Add(origin) Next End If End Sub Public Function GetCorsPolicyAsync(request As Net.Http.HttpRequestMessage) As Threading.Tasks.Task(Of Cors.CorsPolicy) Implements ICorsPolicyProvider.GetCorsPolicyAsync Return Task.FromResult(_policy) End Function End Class
- 在web.config的AppSettings加入CORS相關設置
<appSettings> <!-- 其他省略 --> <add key="internal:origins" value="http://localhost:13466, http://kkbruce.net"/> </appSettings>
這裡我們加入一筆key為internal:origins的設置,代表內部信任的網域。
- 設置EnableCorsAppSettingsAttribute屬性
<EnableCorsAppSettings("internal:origins")> Public Class ValuesController Inherits ApiController ' 其他省略 End Class
重新建置後,符合AppSettings設置的網域都能正常執行CORS的跨網域AJAX請求。
ICorsPolicyProviderFactory介面
ICorsPolicyProviderFactory介面是一個抽象,允許你指定檢索ICorsPolicyProvider介面的方式。預設提供一個實作ICorsPolicyProviderFactory介面的AttributeBasedPolicyProviderFactory類別,允許指定ICorsPolicyProvider作為屬性(<EnableCors>,<DisableCors>)。ICorsPolicyProviderFactory介面定義:
Public Interface ICorsPolicyProviderFactory Function GetCorsPolicyProvider(ByVal request As HttpRequestMessage) As ICorsPolicyProvider End Interface
我們能透過System.Web.Http.Cors命名空間裡的CorsHttpConfigurationExtensions擴充方法裡的SetCorsPolicyProviderFactory方法來註冊自訂ICorsPolicyProviderFactory物件。
Public Shared Class CorsHttpConfigurationExtensions ' 其他省略 Public Shared Sub SetCorsPolicyProviderFactory(ByVal httpConfiguration As HttpConfiguration, ByVal corsPolicyProviderFactory As ICorsPolicyProviderFactory) ' 省略 End Sub End Class
實作ICorsPolicyProviderFactory介面
以下實作ICorsPolicyProviderFactory介面,允許你透過自訂的CorsConfiguration類別來設置相關屬性,而不是使用CORS屬性設置。
Imports System.Web.Http.Cors ''' <summary> ''' 自訂Cors組態類別 ''' </summary> ''' <remarks></remarks> Public Class CorsConfiguration Private _settings As New Dictionary(Of String, EnableCorsAttribute)() ''' <summary> ''' CORS屬性Controller與EnableCors屬性對應設置 ''' </summary> ''' <param name="controller">Controller名稱</param> ''' <param name="policyProvider">EnableCorsAttribute屬性設置</param> Public Sub AddSetting(controller As String, policyProvider As EnableCorsAttribute) _settings.Add(controller, policyProvider) End Sub ''' <summary> ''' 取得Cors屬性清單 ''' </summary> ''' <param name="controller">Controller名稱</param> Public Overridable Function GetPolicyForRequest(controller As String) As EnableCorsAttribute Dim policyProvider As EnableCorsAttribute = Nothing _settings.TryGetValue(controller, policyProvider) Return policyProvider End Function End Class
CorsConfiguration類別我們可以設定每個Controller與EnableCors屬性的對應關係,然後傳給ICorsPolicyProviderFactory介面的實作類別。
Imports System.Net.Http Imports System.Web.Http.Cors ''' <summary> ''' 基於組態的策略工廠 ''' </summary> ''' <remarks></remarks> Public Class ConfigBasedPolicyProviderFactory Implements ICorsPolicyProviderFactory ' 自訂CorsConfiguration類別 Private _configuration As CorsConfiguration ''' <summary> ''' 傳入CorsConfiguration物件,進行初始化工作 ''' </summary> ''' <param name="configuration">CorsConfiguration物件</param> Public Sub New(configuration As CorsConfiguration) _configuration = configuration End Sub ''' <summary> ''' 取得ICorsPolicyProvider屬性清單 ''' </summary> Public Function GetCorsPolicyProvider(request As Net.Http.HttpRequestMessage) As ICorsPolicyProvider Implements ICorsPolicyProviderFactory.GetCorsPolicyProvider Dim routeData = request.GetRouteData() If IsNothing(routeData) OrElse Not routeData.Values.Keys.Contains("controller") Then Return Nothing End If ' 取得Controller名稱 Dim controller = TryCast(routeData.Values("controller"), String) Return _configuration.GetPolicyForRequest(controller) End Function End Class
接下來只需要把設置好的ICorsPolicyProviderFactory介面物件傳送給SetCorsPolicyProviderFactory方法來註冊。
Imports System.Web.Http.Cors Public Class WebApiConfig Public Shared Sub Register(ByVal config As HttpConfiguration) ' 其他省略 Dim corsConfig As New CorsConfiguration() ' 可新增多組Controller與EnableCors屬性的設置 corsConfig.AddSetting("Values", New EnableCorsAttribute With { .Origins = New String() {" http://localhost:13466"}, .Methods = New String() {"DELETE"} }) ' 重要,傳入實作ICorsPolicyProviderFactory介面物件 config.SetCorsPolicyProviderFactory( New ConfigBasedPolicyProviderFactory(corsConfig)) config.EnableCors() End Sub End Class
Web APICORS與Web API Tracing的整合
當我們在WebApiConfig組態中同時啟用config.EnableSystemDiagnosticsTracing()與config.EnableCors(),CORS組件會自動去註冊追蹤器,當用戶端發出跨網域AJAX請求時,可以直接在Visual Studio的輸出視窗查詢相關訊息:
沒有留言:
張貼留言
感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。