CORS解決跨域問題
參考:
http://www.ruanyifeng.com/blog/2016/04/cors.html
https://blog.51cto.com/15089766/2602513
CORS解決跨域問題
一、什麼是CORS
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。跨域資源共享(CORS)是一個瀏覽器和伺服器之間關於跨域問題的協議。
它允許瀏覽器向跨源伺服器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
CORS需要瀏覽器和伺服器同時支援。實現CORS通訊的關鍵是伺服器。
-
瀏覽器端:
目前,所有瀏覽器都支援該功能(IE10以下不行)。整個CORS通訊過程,都是瀏覽器自動完成,不需要使用者參與。
注意:ie下 localhost:8000=localhost:8001=localhost 視為同域,以免本地進行測試的時候踩坑。 -
服務端:
CORS通訊與AJAX沒有任何差別,因此不需要改變以前的業務邏輯。只不過,瀏覽器會在請求中攜帶一些頭資訊,以此判斷是否執行其跨域,然後在響應頭中加入一些資訊即可。這一般通過過濾器完成即可。
二、原理
瀏覽器會將ajax請求分為兩類,其處理方案略有差異:簡單請求(simple request)和 非簡單請求(not-so-simple request)
1. 簡單請求
只要同時滿足以下兩大條件,就屬於簡單請求:
(1) 請求方法是以下三種方法之一:
HEAD
GET
POST
(2)HTTP的頭資訊不超出以下幾種欄位:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
當瀏覽器發現發現的ajax請求是簡單請求時,會在請求頭中攜帶一個欄位:Origin
Origin中會指出當前請求屬於哪個域(協議+域名+埠)。服務會根據這個值決定是否允許其跨域。
① 如果Origin指定的源,不在許可範圍內,伺服器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭資訊沒有包含 Access-Control-Allow-Origin 欄位,就知道出錯了,從而丟擲一個錯誤,被 XMLHttpRequest 的 onerror 回撥函式捕獲。注意,這種錯誤無法通過狀態碼識別,因為HTTP迴應的狀態碼有可能是200。
② 如果Origin指定的域名在許可範圍內,伺服器返回的響應,會多出幾個頭資訊欄位:
Access-Control-Allow-Origin: 一個域名/*
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: XXX
Content-Type: text/html; charset=utf-8
上面的頭資訊之中,有三個與CORS請求相關的欄位,都以Access-Control- 開頭。
Access-Control-Allow-Origin:該欄位是必須的。它的值要麼是請求時Origin欄位的值,要麼是一個*,表示接受任意域名的請求。
Access-Control-Allow-Credentials:該欄位可選。它的值是一個布林值,表示是否允許傳送Cookie。預設情況下,cors不會攜帶cookie。設為true,即表示伺服器明確許可,Cookie可以包含在請求中,一起發給伺服器。這個值也只能設為true,如果伺服器不要瀏覽器傳送Cookie,刪除該欄位即可。
Access-Control-Expose-Headers: 該欄位可選。CORS請求時,XMLHttpRequest物件的 getResponseHeader() 方法只能拿到6個基本欄位:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他欄位,就必須在Access-Control-Expose-Headers裡面指定。上面的例子指定,getResponseHeader(‘XXX’)可以返回XXX欄位的值。
注意:
如果跨域請求要想操作cookie,需要滿足3個條件:
- 服務的響應頭中需要攜帶 Access-Control-Allow-Credentials 並且為 true
- 瀏覽器發起ajax需要指定withCredentials 為 true
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
- 響應頭中的Access-Control-Allow-Origin一定不能為 * ,必須是指定的域名。
注意:Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁程式碼中的document.cookie也無法讀取伺服器域名下的Cookie。
2. 非簡單請求
非簡單請求是那種對伺服器有特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type欄位的型別是application/json。非簡單請求會在正式通訊之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。
預檢請求
瀏覽器先詢問伺服器,當前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些HTTP動詞和頭資訊欄位。只有得到肯定答覆,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。
下面是一段瀏覽器的JavaScript指令碼。
var url = 'http://api.boc.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
上面程式碼中,HTTP請求的方法是PUT,並且傳送一個自定義頭資訊X-Custom-Header。
瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,要求伺服器確認可以這樣請求。下面是這個"預檢"請求的HTTP頭資訊。
OPTIONS /cors HTTP/1.1
Origin: http://api.boc.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.tangyu.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭資訊裡面,關鍵欄位是Origin,表示請求來自哪個源。
除了Origin欄位,"預檢"請求的頭資訊包括兩個特殊欄位。
Access-Control-Request-Method:該欄位是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。
Access-Control-Request-Headers:該欄位是一個逗號分隔的字串,指定瀏覽器CORS請求會額外發送的頭資訊欄位,上例是X-Custom-Header
預檢請求的迴應
-
伺服器收到"預檢"請求以後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers欄位以後,確認允許跨源請求,就可以做出迴應。
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.boc.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
上面的HTTP迴應中,關鍵的是Access-Control-Allow-Origin欄位,表示 http://api.boc.com 可以請求資料。該欄位也可以設為星號,表示同意任意跨源請求。
除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,這裡又額外多出3個頭:
Access-Control-Allow-Methods:該欄位必需,它的值是逗號分隔的一個字串,表明伺服器支援的所有跨域請求的方法。注意: 返回的是所有支援的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次"預檢"請求。
Access-Control-Allow-Headers:如果瀏覽器請求包括Access-Control-Request-Headers欄位,則 Access-Control-Allow-Headers 欄位是必需的。它也是一個逗號分隔的字串,表明伺服器支援的所有頭資訊欄位,不限於瀏覽器在"預檢"中請求的欄位。
Access-Control-Max-Age:該欄位可選,用來指定本次預檢請求的有效期,單位為秒,由服務端和瀏覽器預設值共同決定。在此期間,不用發出另一條預檢請求。
-
如果伺服器否定了"預檢"請求,會返回一個正常的HTTP迴應,但是沒有任何CORS相關的頭資訊欄位。這時,瀏覽器就會認定,伺服器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest物件的onerror回撥函式捕獲。控制檯會打印出如下的報錯資訊。
XMLHttpRequest cannot load http://api.tangyu.com. Origin http://api.boc.com is not allowed by Access-Control-Allow-Origin.
瀏覽器的正常請求和迴應
一旦伺服器通過了"預檢"請求,以後每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭資訊欄位。伺服器的迴應,也都會有一個Access-Control-Allow-Origin頭資訊欄位。
提升效能
預檢請求,在大多數情況下,它會對響應時間造成很大的延遲,從而影響 web 應用程式的效能。
繞過預檢請求或者減少預檢響應時間,以提高 web 應用程式的效能。
1. 使用瀏覽器的預檢快取
如前所述,預檢請求對應用程式效能有影響。根據前端呼叫 API 的數量,很可能會發送許多預檢請求。
作為一種解決方案,預檢快取是減少影響的常用方法之一。這背後的原理很簡單。
預檢快取的行為與任何其他快取機制類似。每當瀏覽器發出預檢請求時,它首先檢查預檢快取,看看是否有對該請求的響應。如果瀏覽器找到了響應,它不會向伺服器傳送預檢請求,而是使用快取的響應。只有在預檢快取中沒有找到響應時,瀏覽器才會傳送預檢請求。
Access-Control-Max-Age 響應頭表示結果可以在瀏覽器快取中快取多長時間。
2. 使用代理、閘道器或負載均衡實現伺服器端快取
在前面的方法中,我們討論了在瀏覽器中快取預檢請求的方法,現在我們來看看伺服器端快取。
儘管這種方法不是專門用於預檢請求快取,但我們可以使用代理、閘道器甚至像 AWS CloudFront 這樣的 CDN 的預設快取機制來減少預檢請求延遲時間。
其思想就是通過縮短預檢請求的傳輸距離來減少響應時間。
例如,以 AWS CloudFront CDN 為示例。它是一個代理,使用了一種被稱為邊緣位置(比原始伺服器更接近使用者的瀏覽器)的概念來攔截 HTTP 請求。
在這裡,可以在邊緣位置附近快取預檢響應,這樣預檢請求甚至不需要訪問源伺服器。
3. 使用代理、閘道器或負載均衡避免預檢請求
可以通過同一個域同時服務前端和後端,我們就可以完全避免預檢請求,因為此時不存在 CORS。
假設正在本地環境開發一個應用, 前端執行在 http://localhost:4200,後端執行在 http://localhost:3000/api。
必須在後端開啟 CORS 才能在二者之間通訊。但是,可以在前端配置簡單的代理以在前後端之間形成對映,這樣就可以完全避免 CORS。
只需要定義一個代理配置來轉發前往 http://localhost:3000 的 /api 路徑請求。然後在前端(http://localhost:4200/api/…)就可以請求同一域名下的後端 API,此時瀏覽器不會再發送任何預檢請求。
在生產環境可以使用 API 閘道器,負載均衡,代理或者 CDN,比如 NGINX,Traefik,AWS CloudFront,AWS Application Load Balancer,Azure Application Gateway 來做基於路由的配置。
4. 簡單請求
另一種避免預檢請求的方法是使用簡單請求。但是,簡單請求的限制對於現代的 web 應用程式來說太過嚴格,我們不能限定在這些範圍之內來為客戶提供最佳的解決方案。例如,在簡單請求中不允許使用授權頭,現在幾乎所有的 HTTP 請求都在使用授權頭。
建議
建議只在必要時才使用 CORS,因為與啟用後端 API 的同源訪問來改善專案延遲的工作相比,我們可以節省大量開發時間。在這種情況下,可以很容易地使用代理配置、API 閘道器或負載均衡來減少麻煩。
但有些情況下無法避免 CORS。此時,可以簡單地遵循瀏覽器快取或伺服器端快取機制來最小化響應時間。