【WEB】瀏覽器同源策略及跨域資源共享(CORS)

歡迎關注微信公眾號:全棧工廠

本文主要參考

瀏覽器的同源策略一直以來都是Web安全的基礎,但這同時也制約了Web研發的一部分功能,特別是現在大多數網站都采用前后端分離的架構,因此很多時候前端需要跨域去獲取一些資源或者接口數據,這時候就不得不想辦法規避瀏覽器的同源策略。作為一名合格的Coder,腫么可以不去好好了解它咧~~~

一、同源策略(Same-origin policy)

1.1 概述

打個簡單的比喻就是:如果我在瀏覽器同時訪問新浪和搜狐兩個網站并且這兩個網站都在瀏覽器存儲了用戶名和密碼,那么他們各自只能訪問各自存儲的用戶信息,無法交叉訪問,否則就會造成用戶信息泄露(現在同源策略的限制遠不止這一點)。

1.2 定義

一個URL的通用格式由9部分構成(想深入了解的童鞋請看《HTTP權威指南》第二章):

  • <scheme[方案協議]>://<user[用戶名]>:<password[密碼]>@<host[主機域名]>:<port[端口號]>/<path[路徑]>;<params[參數]>?<query[查詢]>#<frag[片段]>

對于同源的定義主要看三點:

  1. 協議相同
  2. 域名相同
  3. 端口相同

舉例來說, http://www.lxweimin.com/p/43362635aee2 這個網址中, http 是協議, www.lxweimin.com 是域名,端口是80(http協議默認端口可以省略,你也可以把他看成:http://www.lxweimin.com:80/p/43362635aee2)。任意兩個網址,只要這三點全部相同,那么瀏覽器就認為它們是同源的,任意一個不相同都會被瀏覽器認為是跨域。

注:IE例外

IE瀏覽器對于同源策略有兩個主要的例外:

  1. 授信范圍(Trust Zones):兩個相互之間高度互信的域名,如公司域名(corporate domains),不遵守同源策略的限制。
  2. 端口:IE瀏覽器沒有將端口號加入到同源策略的組成部分之中,因此 http://example.com:81/index.htmlhttp://example.com/index.html 屬于同源并且不受任何限制。
1.3 限制范圍

如果兩個請求非同源,將會受到以下三種行為限制:

  1. Cookie、LocalStorage 和 IndexDB 無法讀取。
  2. DOM 無法獲得。
  3. AJAX 請求不能發送。

二、跨域網絡訪問

2.1 源修改——共享Cookie

頁面可能會更改其自己的來源,但有一些限制。腳本可以將document.domain的值設置為其當前域或其當前域的超級域,這樣可以實現所有一級域名相同的網頁一起共享Cookie。
舉例來說,A網頁是http://w1.example.com/a.html,B網頁是http://w2.example.com/b.html,那么只要設置相同的document.domain,兩個網頁就可以共享Cookie。

document.domain = 'example.com';

現在,A網頁通過腳本設置一個 Cookie。

document.cookie = "test1=hello";

B網頁就可以讀到這個 Cookie。

var allCookie = document.cookie;
2.2 window.postMessage——跨文檔通信

HTML5引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。這個API為window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。
舉例來說,父窗口http://aaa.com向子窗口http://bbb.com發消息,調用postMessage方法就可以了。

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一個參數是具體的信息內容,第二個參數是接收消息的窗口的源(origin),即"協議 + 域名 + 端口"。也可以設為*,表示不限制域名,向所有窗口發送。
子窗口向父窗口發送消息的寫法類似。

window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口都可以通過message事件,監聽對方的消息。

 window.addEventListener('message', function(e) {
   console.log(e.data);
 },false);

message事件的事件對象event,提供以下三個屬性。

  • event.source:發送消息的窗口
  • event.origin: 消息發向的網址
  • event.data: 消息內容

下面的例子是,子窗口通過event.source屬性引用父窗口,然后發送消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
 event.source.postMessage('Nice to see you!', '*');
} 

event.origin屬性可以過濾不是發給本窗口的消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
} 
2.3 JSONP——跨域通信

JSONP跨域通信是利用<script>標簽沒有跨域限制的“漏洞”來達到與第三方通訊的目的。它的基本思想是,網頁通過添加一個<script>元素,向服務器請求JSON數據,服務器收到請求后,將數據以json形式進行包裝(故稱之為jsonp,即json padding),然后放在一個指定名字的回調函數里傳回來,形如:callback({"name":"Liqing","nickname":"Newbie"})這樣瀏覽器會調用callback函數,并傳遞解析后json對象作為參數。
首先,本站腳本創建一個<script>元素,由它向跨源網址發出請求:

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

上面代碼通過動態添加<script>元素,向服務器example.com發出請求。注意,該請求的查詢字符串有一個callback參數,用來指定回調函數的名字,這對于JSONP是必需的。
服務器收到這個請求以后,會將數據放在回調函數的參數位置返回。

foo({
  "ip": "8.8.8.8"
});

由于<script>元素請求的腳本,直接作為代碼運行。這時,只要瀏覽器定義了foo函數,該函數就會立即調用。作為參數的JSON數據被視為JavaScript對象,而不是字符串,因此避免了使用JSON.parse的步驟。

2.4 WebSocket——跨域通信

WebSocket是HTML5開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。在WebSocket API中,瀏覽器和服務器只需要做一個握手的動作,然后,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。
WebSocket目前由W3C進行標準化。WebSocket已經受到Firefox 4、Chrome 4、Opera 10.70以及Safari 5等瀏覽器的支持。
WebSocket API最偉大之處在于服務器和客戶端可以在給定的時間范圍內的任意時刻,相互推送信息。WebSocket并不限于以Ajax(或XHR)方式通信,因為Ajax技術需要客戶端發起請求,而WebSocket服務器和客戶端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允許跨域通信。

三、跨域Ajax之根本解決方案——跨域資源共享(Cross-origin resource sharing)

跨域資源共享(CORS)是一個W3C標準,可以說它的誕生就是為了解決Ajax跨域請求問題的。CORS與JSONP相比:

  1. JSONP只能實現GET請求,而CORS支持所有類型的HTTP請求。
  2. 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得數據,比起JSONP有更好的錯誤處理。
  3. JSONP主要被老的瀏覽器支持,但它們往往不支持CORS,而所有現代瀏覽器都支持CORS(IE瀏覽器不能低于IE10)。
3.1 概述

整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨域通信。
瀏覽器的CORS請求主要分為兩種:簡單請求非簡單請求,只要同時滿足以下兩大條件,就屬于簡單請求。
① 請求方法是以下三種方法之一:

  • HEAD
  • GET
  • POST

② HTTP的頭信息不超出以下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三個值application/x-www-form-urlencoded、multipart/form-data、 text/plain

凡是不同時滿足上面兩個條件,就屬于非簡單請求,一個非簡單請求不僅有包含通信內容的請求,同時也包含預請求(preflight request,即:請求兩次)。

3.2 簡單請求

簡單請求的發送從代碼上來看和普通的XHR沒太大區別,但是HTTP頭當中要求總是包含一個域(Origin)的信息。該域包含協議名、地址以及一個可選的端口。不過這一項實際上由瀏覽器代為發送,并不是開發者代碼可以觸及到的,例如:

GET  /source HTTP/1.1
Origin: http://api.test.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的頭信息中,Origin字段用來說明,本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。如果服務端返回的HTTP響應頭中沒有Access-Control-Allow-Origin字段,那么瀏覽器便會認為沒有跨域訪問該資源的權限,拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤無法通過狀態碼識別,因為HTTP回應的狀態碼有可能是200。
如果Origin指定的域名在許可范圍內,服務器返回的響應,會多出幾個頭信息字段。

  • Access-Control-Allow-Origin(必須)- 不可省略,否則請求按失敗處理。該項控制數據的可見范圍,如果希望數據對任何人都可見,可以填寫"*"。
  • Access-Control-Allow-Credentials(可選) – 該項標志著請求當中是否包含cookies信息,只有一個可選值:true(必為小寫)。如果不包含cookies,請略去該項,而不是填寫false。這一項與XmlHttpRequest2對象當中的withCredentials屬性應保持一致,即withCredentials為true時該項也為true;withCredentials為false時,省略該項不寫。反之則導致請求失敗。
  • Access-Control-Expose-Headers(可選) – 該項確定XmlHttpRequest2對象當中getResponseHeader()方法所能獲得的額外信息。通常情況下,getResponseHeader()方法只能獲得如下的信息:
    上面說到,CORS請求默認不發送Cookie和HTTP認證信息。如果要把Cookie發到服務器,一方面要服務器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true

另一方面,開發者必須在AJAX請求中打開withCredentials屬性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否則,即使服務器同意發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。
但是,如果省略withCredentials設置,有的瀏覽器還是會一起發送Cookie。這時,可以顯式關閉withCredentials

xhr.withCredentials = false;

需要注意的是,如果要發送Cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie才會上傳,其他域名的Cookie并不會上傳,且(跨源)原網頁代碼中的document.cookie也無法讀取服務器域名下的Cookie。

3.3非簡單請求
3.3.1預檢請求

非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為預檢請求(preflight)。
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。
預請求以OPTIONS形式發送,當中同樣包含域,并且還包含了兩項CORS特有的內容:

  • Access-Control-Request-Method – 該項內容是實際請求的種類,可以是GET、POST之類的簡單請求,也可以是PUT、DELETE等等。
  • Access-Control-Request-Headers – 該項是一個以逗號分隔的列表,當中是復雜請求所使用的頭部。
    上面代碼中,HTTP請求的方法是PUT,并且發送一個自定義頭信息X-Custom-Header

瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,要求服務器確認可以這樣請求。下面是這個"預檢"請求的HTTP頭信息。

OPTIONS /source HTTP/1.1
Origin: http://api.test.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...

"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息里面,關鍵字段是Origin,表示請求來自哪個源。

除了Origin字段,"預檢"請求的頭信息包括兩個特殊字段。

① Access-Control-Request-Method

該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT

② Access-Control-Request-Headers

該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,上例是X-Custom-Header
顯而易見,這個預請求實際上就是在為之后的實際請求發送一個權限請求,在預回應返回的內容當中,服務端應當對這兩項進行回復,以讓瀏覽器確定請求是否能夠成功完成。

3.3.2 預檢請求預檢請求的回應

服務器收到"預檢"請求以后,檢查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,確認允許跨源請求,就可以做出回應。

非簡單請求的部分響應頭及解釋如下:

  • Access-Control-Allow-Origin(必含) – 和簡單請求一樣的,必須包含一個域。
  • Access-Control-Allow-Methods(必含) – 這是對預請求當中Access-Control-Request-Method的回復,這一回復將是一個以逗號分隔的列表。盡管客戶端或許只請求某一方法,但服務端仍然可以返回所有允許的方法,以便客戶端將其緩存。
  • Access-Control-Allow-Headers(當預請求中包含Access-Control-Request-Headers時必須包含) – 這是對預請求當中Access-Control-Request-Headers的回復,和上面一樣是以逗號分隔的列表,可以返回所有支持的頭部。這里在實際使用中有遇到,所有支持的頭部一時可能不能完全寫出來,而又不想在這一層做過多的判斷,沒關系,事實上通過request的header可以直接取到Access-Control-Request-Headers,直接把對應的value設置到Access-Control-Allow-Headers即可。
  • Access-Control-Allow-Credentials(可選) – 和簡單請求當中作用相同。
  • Access-Control-Max-Age(可選) – 以秒為單位的緩存時間。預請求的的發送并非免費午餐,允許時應當盡可能緩存。

如果瀏覽器否定了"預檢"請求,會返回一個正常的HTTP回應,但是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲。

3.3.3 瀏覽器的正常請求和回應

一旦服務器通過了"預檢"請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin頭信息字段。
下面是"預檢"請求之后,瀏覽器的正常CORS請求。

PUT /source HTTP/1.1
Origin: http://api.test.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面頭信息的Origin字段是瀏覽器自動添加的。
下面是服務器正常的回應。

Access-Control-Allow-Origin: http://api.test.com
Content-Type: text/html; charset=utf-8

上面頭信息中,Access-Control-Allow-Origin字段是每次回應都必定包含的。

注:文中如有任何錯誤,請各位批評指正!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容