完全理解瀏覽器的跨域

同源策略

1995年,同源政策由 Netscape 公司引入瀏覽器。目前,全部瀏覽器都實行這個政策。html

最初,它的含義是指,A 網頁設置的 Cookie,B 網頁不能打開,除非這兩個網頁「同源」。所謂「同源」指的是「三個相同」:前端

  • 協議相同
  • 域名相同
  • 端口相同

同源政策的目的,是爲了保證用戶信息的安全,防止惡意的網站竊取數據。java

設想這樣一種狀況:A 網站是一家銀行,用戶登陸之後,A 網站在用戶的機器上設置了一個 Cookie,包含了一些隱私信息(好比存款總額)。用戶離開 A 網站之後,又去訪問 B 網站,若是沒有同源限制,B 網站能夠讀取 A 網站的 Cookie,那麼隱私信息就會泄漏。更可怕的是,Cookie 每每用來保存用戶的登陸狀態,若是用戶沒有退出登陸,其餘網站就能夠冒充用戶,隨心所欲。由於瀏覽器同時還規定,提交表單不受同源政策的限制。webpack

因而可知,同源政策是必需的,不然 Cookie 能夠共享,互聯網就毫無安全可言了。web

隨着互聯網的發展,同源政策愈來愈嚴格。目前,若是非同源,共有三種行爲受到限制。json

  1. 沒法獲取非同源網頁的 cookie、localstorage 和 indexedDB。
  2. 沒法訪問非同源網頁的 DOM (iframe)。
  3. 沒法向非同源地址發送 AJAX 請求 或 fetch 請求(能夠發送,但瀏覽器拒絕接受響應)。

Ajax 跨域

瀏覽器的同源策略會致使跨域,也就是說,若是協議、域名或者端口有一個不一樣,都被看成是不一樣的域,就不能使用 Ajax 向不一樣源的服務器發送 HTTP 請求。首先咱們要明確一個問題,請求跨域了,請求到底發出去沒有?答案是確定發出去了,可是瀏覽器攔截了響應。segmentfault

爲何要有跨域

Ajax 的同源策略主要是爲了防止 CSRF(跨站請求僞造) 攻擊,若是沒有 AJAX 同源策略,至關危險,咱們發起的每一次 HTTP 請求都會帶上請求地址對應的 cookie,那麼能夠作以下攻擊:後端

  1. 用戶登陸了本身的銀行頁面 mybank.commybank.com向用戶的cookie中添加用戶標識
  2. 用戶瀏覽了惡意頁面 evil.com。執行了頁面中的惡意AJAX請求代碼。
  3. evil.com向http://mybank.com發起AJAX HTTP請求,請求會默認把http://mybank.com對應cookie也同時發送過去。
  4. 銀行頁面從發送的cookie中提取用戶標識,驗證用戶無誤,response中返回請求數據。此時數據就泄露了。
  5. 並且因爲Ajax在後臺執行,用戶沒法感知這一過程。

DOM同源策略也同樣,若是 iframe 之間能夠跨域訪問,能夠這樣攻擊:api

  1. 作一個假網站,裏面用iframe嵌套一個銀行網站 mybank.com
  2. 把iframe寬高啥的調整到頁面所有,這樣用戶進來除了域名,別的部分和銀行的網站沒有任何差異。
  3. 這時若是用戶輸入帳號密碼,咱們的主網站能夠跨域訪問到http://mybank.com的dom節點,就能夠拿到用戶的輸入了,那麼就完成了一次攻擊。

因此說有了跨域跨域限制以後,咱們才能更安全的上網了。跨域

跨域的解決方式

CORS

CORS 是一個 W3C 標準,全稱是跨域資源共享(Cross-origin resource sharing),它容許瀏覽器向跨源服務器,發出XMLHttpRequest請求。

整個 CORS 通訊過程,都是瀏覽器自動完成,不須要用戶參與。對於開發者來講,CORS 通訊與普通的 AJAX 通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現 AJAX 請求跨域,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感知。所以,實現 CORS 通訊的關鍵是服務器。只要服務器實現了 CORS 接口,就能夠跨域通訊。

服務器端配置

CORS經常使用的配置項有如下幾個:

  • Access-Control-Allow-Origin(必含) – 容許的域名,只能填 *(通配符)或者單域名。

  • Access-Control-Allow-Methods(必含) – 這容許跨域請求的 http 方法(常見有 POST、GET、OPTIONS)。

  • Access-Control-Allow-Headers(當預請求中包含 Access-Control-Request-Headers 時必須包含) – 這是對預請求當中 Access-Control-Request-Headers 的回覆,和上面同樣是以逗號分隔的列表,能夠返回全部支持的頭部。

  • Access-Control-Allow-Credentials(可選) – 表示是否容許發送Cookie,只有一個可選值:true(必爲小寫)。若是不包含cookies,請略去該項,而不是填寫false。這一項與 XmlHttpRequest 對象當中的 withCredentials 屬性應保持一致,即 withCredentials 爲true時該項也爲true;withCredentials 爲false時,省略該項不寫。反之則致使請求失敗。

  • Access-Control-Max-Age(可選) – 以秒爲單位的緩存時間。在有效時間內,瀏覽器無須爲同一請求再次發起預檢請求。

CORS 跨域的斷定流程

  1. 瀏覽器先根據同源策略對前端頁面和後臺交互地址作匹配,若同源,則直接發送數據請求;若不一樣源,則發送跨域請求。

  2. 服務器收到瀏覽器跨域請求後,根據自身配置返回對應文件頭。若未配置過任何容許跨域,則文件頭裏不包含 Access-Control-Allow-origin 字段,若配置過域名,則返回 Access-Control-Allow-origin + 對應配置規則裏的域名的方式

  3. 瀏覽器根據接受到的 響應頭裏的 Access-Control-Allow-origin 字段作匹配,若無該字段,說明不容許跨域,從而拋出一個錯誤;如有該字段,則對字段內容和當前域名作比對,若是同源,則說明能夠跨域,瀏覽器接受該響應;若不一樣源,則說明該域名不可跨域,瀏覽器不接受該響應,並拋出一個錯誤。

上面說到的兩種類型的報錯,控制檯輸出是不同的:

  1. 服務器容許跨域請求,可是 Origin 指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含 Access-Control-Allow-Origin 字段,就知道出錯了,從而拋出一個錯誤,被 XMLHttpRequest的onerror 回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於 HTTP 迴應的狀態碼有多是200。
<!--控制檯返回結果-->
 XMLHttpRequest cannot load http://localhost/city.json.
 The 'Access-Control-Allow-Origin' header has a value 'http://segmentfault.com' that is not equal to the supplied origin. 
 Origin 'http://www.zhihu.com' is therefore notallowed access.
複製代碼
  1. 服務器不容許任何跨域請求
<!--控制檯返回結果-->
XMLHttpRequest cannot load http://localhost/city.json.
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://www.zhihu.com' is therefore not allowed access.
複製代碼

簡單請求

實際上瀏覽器將CORS請求分紅兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

簡單請求是指知足如下條件的(通常只考慮前面兩個條件便可):

  1. 使用 GET、POST、HEAD 其中一種請求方法。
  2. HTTP的頭信息不超出如下幾種字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain
  3. 請求中的任意XMLHttpRequestUpload 對象均沒有註冊任何事件監聽器;
  4. XMLHttpRequestUpload 對象可使用 XMLHttpRequest.upload 屬性訪問。 請求中沒有使用 ReadableStream 對象。

對於簡單請求,瀏覽器直接發起 CORS 請求,具體來講就是服務器端會根據請求頭信息中的 origin 字段(包括了協議 + 域名 + 端口),來決定是否贊成此次請求。

若是 origin 指定的源在許可範圍內,服務器返回的響應,會多出幾個頭信息字段:

Access-Control-Allow-Origin: http://xxx.xxx.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
複製代碼

非簡單請求

非簡單請求時指那些對服務器有特殊要求的請求,好比請求方法是 putdelete,或者 content-type 的類型是 application/json。其實簡單請求以外的都是非簡單請求了。

非簡單請求的 CORS 請求,會在正式通訊以前,使用 OPTIONS 方法發起一個預檢(preflight)請求到服務器,瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些 HTTP 動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的 XMLHttpRequest 請求,不然就報錯。

下面是一個預檢請求的頭部:

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
複製代碼

一旦服務器經過了"預檢"請求,之後每次瀏覽器正常的CORS請求,就都跟簡單請求同樣了。

關於爲何要有簡單請求和非簡單請求,可參考知乎上的一個回答 爲何跨域的post請求區分爲簡單請求和非簡單請求和content-type相關?

JSONP

JSONP 的原理就是利用 <script> 標籤的 src 屬性沒有跨域的限制,經過指向一個須要訪問的地址,由服務端返回一個預先定義好的 Javascript 函數的調用,而且將服務器數據以該函數參數的形式傳遞過來,此方法須要先後端配合完成。

//定義獲取數據的回調方法
function getData(data) {
  console.log(data);
}

// 建立一個script標籤,而且告訴後端回調函數名叫 getData
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.type = 'text/javasctipt';
script.src = 'demo.js?callback=getData';
body.appendChild(script);

//script 加載完畢以後從頁面中刪除,不然每次點擊生成許多script標籤
script.onload = function () {
  document.body.removeChild(script);
}
複製代碼

JSONP 使用簡單且兼容性不錯,可是隻限於 get 請求。

服務器代理

瀏覽器有跨域限制,可是服務器不存在跨域問題,因此能夠由服務器請求所要域的資源再返回給客戶端。

通常咱們在本地環境開發時,就是使用 webpack-dev-server 在本地開啓一個服務進行代理訪問的。

document.domain

該方式只能用於二級域名相同的狀況下,好比 a.test.comb.test.com 適用於該方式。

只須要給兩個頁面都添加 document.domain = 'test.com',經過在 a.test.com 建立一個 iframe,去控制 iframewindow,從而進行交互。

postMessage

window.postMessage 是一個 HTML5 的 api,容許兩個窗口之間進行跨域發送消息。

這種方式一般用於獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另外一個頁面判斷來源並接收消息

// 發送消息端
var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('send');
btn.addEventListener('click', function (e) {
    e.preventDefault();
    var val = document.getElementById('text').value;
    receiver.postMessage("Hello "+val+"!", "http://res.42du.cn");
}); 

// 接收消息端
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
  if (event.origin !== "http://www.42du.cn")
    return;
}
複製代碼

詳情可參考 MDN | window.postMessage

還有一些方法,好比window.name和location.hash。都比較適用於 iframe 的跨域,不過 iframe 用的比較少了,因此這些方法也就有點過期了。

參考資料

跨域資源共享 CORS 詳解

跨域的那些事兒

MDN | HTTP訪問控制(CORS)

相關文章
相關標籤/搜索