徹底搞清瀏覽器跨域問題
這里說的跨域是瀏覽器特有的,服務器則不存在
同源策略
最初,它的含義是指,A 網頁設置的 Cookie,B 網頁不能打開,除非這兩個網頁“同源”。所謂“同源”指的是“三個相同”:
- 協議相同
- 域名相同
- 端口相同
同源政策的目的,是為了保證用戶信息的安全,防止惡意的網站竊取數據。
設想這樣一種情況:A 網站是一家銀行,用戶登錄以后,A 網站在用戶的機器上設置了一個 Cookie,包含了一些隱私信息(比如存款總額)。用戶離開 A 網站以后,又去訪問 B 網站,如果沒有同源限制,B 網站可以讀取 A 網站的 Cookie,那么隱私信息就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為。因為瀏覽器同時還規定,提交表單不受同源政策的限制。
隨着互聯網的發展,同源政策越來越嚴格。目前,如果非同源,共有三種行為受到限制
- 無法獲取非同源網頁的 cookie、localstorage 和 indexedDB。
- 無法訪問非同源網頁的 DOM (iframe)。
- 無法向非同源地址發送 AJAX 請求 或 fetch 請求(可以發送,但瀏覽器拒絕接受響應)。
Ajax 跨域
瀏覽器的同源策略會導致跨域,也就是說,如果協議、域名或者端口有一個不同,都被當作是不同的域,就不能使用 Ajax 向不同源的服務器發送 HTTP 請求。首先我們要明確一個問題,請求跨域了,請求到底發出去沒有?答案是肯定發出去了,但是瀏覽器攔截了響應。
為什么要有跨域
Ajax 的同源策略主要是為了防止 CSRF(跨站請求偽造) 攻擊,如果沒有 AJAX 同源策略,相當危險,我們發起的每一次 HTTP 請求都會帶上請求地址對應的 cookie,那么可以做如下攻擊:
-
用戶登錄了自己的銀行頁面 mybank.com,mybank.com向用戶的cookie中添加用戶標識。
-
用戶瀏覽了惡意頁面 evil.com。執行了頁面中的惡意AJAX請求代碼。
-
evil.com向http://mybank.com發起AJAX HTTP請求,請求會默認把http://mybank.com對應cookie也同時發送過去。
-
銀行頁面從發送的cookie中提取用戶標識,驗證用戶無誤,response中返回請求數據。此時數據就泄露了。
-
而且由於Ajax在后台執行,用戶無法感知這一過程。
DOM同源策略也一樣,如果 iframe 之間可以跨域訪問,可以這樣攻擊:
-
做一個假網站,里面用iframe嵌套一個銀行網站 mybank.com。
-
把iframe寬高啥的調整到頁面全部,這樣用戶進來除了域名,別的部分和銀行的網站沒有任何差別。
-
這時如果用戶輸入賬號密碼,我們的主網站可以跨域訪問到http://mybank.com的dom節點,就可以拿到用戶的輸入了,那么就完成了一次攻擊。
跨域的解決方式
CORS
CORS 是一個 W3C 標准,全稱是跨域資源共享(Cross-origin resource sharing),它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求。
整個 CORS 通信過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS 通信與普通的 AJAX 通信沒有差別,代碼完全一樣。瀏覽器一旦發現 AJAX 請求跨域,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感知。因此,實現 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 跨域的判定流程:
- 瀏覽器先根據同源策略對前端頁面和后台交互地址做匹配,若同源,則直接發送數據請求;若不同源,則發送跨域請求。
- 服務器收到瀏覽器跨域請求后,根據自身配置返回對應文件頭。若未配置過任何允許跨域,則文件頭里不包含 Access-Control-Allow-origin 字段,若配置過域名,則返回 Access-Control-Allow-origin + 對應配置規則里的域名的方式。
- 瀏覽器根據接受到的 響應頭里的 Access-Control-Allow-origin 字段做匹配,若無該字段,說明不允許跨域,從而拋出一個錯誤;若有該字段,則對字段內容和當前域名做比對,如果同源,則說明可以跨域,瀏覽器接受該響應;若不同源,則說明該域名不可跨域,瀏覽器不接受該響應,並拋出一個錯誤。
- 上面說到的兩種類型的報錯,控制台輸出是不一樣的:
- 服務器允許跨域請求,但是 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.
- 服務器不允許任何跨域請求
<!--控制台返回結果-->
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)。
- 簡單請求是指滿足以下條件的(一般只考慮前面兩個條件即可):
- 使用 GET、POST、HEAD 其中一種請求方法。
- HTTP的頭信息不超出以下幾種字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain
- 請求中的任意XMLHttpRequestUpload 對象均沒有注冊任何事件監聽器;
- 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
- 非簡單請求
非簡單請求時指那些對服務器有特殊要求的請求,比如請求方法是 put 或 delete,或者 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請求,就都跟簡單請求一樣了。
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.com 和 b.test.com 適用於該方式。
只需要給兩個頁面都添加 document.domain = 'test.com',通過在 a.test.com 創建一個 iframe,去控制 iframe 的 window,從而進行交互。
- postMessage
window.postMessage 是一個 HTML5 的 api,允許兩個窗口之間進行跨域發送消息。
這種方式通常用於獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另一個頁面判斷來源並接收消息