1. 程式人生 > 其它 >CORS解決跨域問題

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通訊的關鍵是伺服器。

只要伺服器實現了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

預檢請求的迴應

  1. 伺服器收到"預檢"請求以後,檢查了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該欄位可選,用來指定本次預檢請求的有效期,單位為秒,由服務端和瀏覽器預設值共同決定。在此期間,不用發出另一條預檢請求。

  2. 如果伺服器否定了"預檢"請求,會返回一個正常的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。此時,可以簡單地遵循瀏覽器快取或伺服器端快取機制來最小化響應時間。