詳解瀏覽器跨域

原文鏈接:https://my.oschina.net/u/4203303/blog/3102954

 

一、什麼是跨域?

JavaScript出於安全方面的考慮做的同源策略的限制,不允許跨域訪問其他資源。通常跨域請求成功後,瀏覽器會拒絕響應服務器端返回的結果。

1.出於哪些方面的安全考慮?

同源政策的目的是爲了防止惡意網站竊取用戶數據信息冒充用戶做一些操作。同源限制只是提高攻擊成本。如果沒有JavaScript同源限制:

(1)CSRF攻擊

(2)XSS攻擊

2.什麼是同源?

域名、協議、端口均相同。舉例來說,http://www.example.com/dir/page.html這個網址,協議是http://,域名是www.example.com,端口是80(默認端口可以省略)

3.做了哪些限制?

(1)Window對象之間的跨源通信:無法讀取Cookie、LocalStorage 、IndexDB 和獲取DOM,但通過以下標籤可以跨域訪問資源:

<img src="URL">
<link href="URL">
<script src="URL">
<iframe src="URL">
<form action="URL" method="get/post">
  First name: <input type="text" name="fname"><br>
  Last name: <input type="text" name="lname"><br>
  <input type="submit" value="提交">
</form>

另外,如果是非同源的網頁,目前允許通過 JavaScript 腳本可以拿到其他窗口/網頁的window對象的九個屬性和四個方法。

window.closed
window.frames
window.length
window.location
window.opener
window.parent
window.self
window.top
window.window
window.blur()
window.close()
window.focus()
window.postMessage()

其中,只有window.location是可讀寫(非同源的情況下,也只允許調用location.replace方法和寫入location.href屬性)的,其他八個全部都是隻讀。

(3)客戶端服務器間:瀏覽器拒絕接受AJAX 請求的響應。

4.怎麼樣算是跨域?

如下相對http://store.company.com/dir/page.html同源檢測的示例:

URL 結果 原因
http://store.company.com/dir2/other.html 成功  只有路徑不同
http://store.company.com/dir/inner/another.html 成功  只有路徑不同
https://store.company.com/secure.html 失敗 不同協議 ( https和http )
http://store.company.com:81/dir/etc.html 失敗 不同端口 ( http:// 80是默認的)
http://news.company.com/dir/other.html 失敗 不同域名 ( news和store )

 

注意:域名與其對應的ip也不能成功訪問

二、如何解決跨域限制?

1.Window對象之間的跨源通信

(1)如果兩個網頁只是二級域名不同,一級域名相同

① 瀏覽器允許通過設置document.domain共享 Cookie,拿到 DOM。

/****A網頁:http://t1.example.com/a.html*/

document.domain = 'example.com';
//設置cookie
document.cookie = "test1=hello";


/****B網頁:http://t2.example.com/b.html,設置相同的document.domain*/

document.domain = 'example.com';
//訪問A網頁的cookie
console.log(document.cookie);

/*注意:A 和 B 兩個網頁都需要設置document.domain屬性,才能達到同源的目的。因爲設置document.domain的同時,會把端口重置爲null,因此如果只設置一個網頁的document.domain,會導致兩個網址的端口不同,還是達不到同源的目的*/

② 另外,服務器也可以在設置Cookie的時候,指定Cookie的所屬域名爲一級域名,二/三級域名不用做任何設置,就可以讀取這個Cookie

Set-Cookie: key=value; domain=.example.com; path=/

缺點:這種方法只適用於共享 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 無法通過這種方法跨域共享

(2)對於完全不同源的網站,可以通過以下三種解決跨域窗口的通信問題:

①片段識別符(URL的#號後面的部分)

只是改變url的片段標識符,頁面不會重新刷新,父窗口可以把信息,寫入子窗口的片段標識符。

//****父窗口把要共享的信息添加到子窗口url的#後

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;



//****子窗口監聽窗口變化

window.onhashchange = ()=>{
//獲取url的#後的數據
  var data= window.location.hash;
}
//子窗口也可以通過這種方式向父窗口共享信息
parent.location.href= target + "#" + hash;

②window.name

瀏覽器窗口的window.name屬性,只要在同一個窗口裏無論是否同源,前一個網頁設置了這個屬性,後一個網頁可以讀取它,且容量很大,可以放置非常長的字符串。

//****子窗口:http://child.url.com/xxx.html,將信息寫入window.name屬性:

window.name = data;

location = 'http://parent.url.com/other.html';//接着,子窗口跳回一個與父窗口同域的網址。



//****父窗口:http://parent.url.com/xxx.html,先打開不同源的子窗口網頁:

var iframe = document.createElement('iframe');

iframe.id='myFrame';

iframe.src = 'http://child.url.com/xxx.html';//iframe可以跨域加載資源

document.body.appendChild(iframe);

//然後,父窗口就可以讀取子窗口的window.name了。

var data = document.getElementById('myFrame').contentWindow.name;

缺點:必須監聽子窗口window.name屬性的變化,影響網頁性能。

③window.postMessage

HTML5爲了解決Window對象之間的跨源通信問題(例如:在頁面和它的彈出窗口之間,或嵌入其中的iframe之間,具體參見:https://www.w3cschool.cn/fetch_api/fetch_api-lx142x8t.html),引入了——跨文檔通信 API。這個API爲window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。

/*語法:otherWindow.postMessage(message, targetOrigin, [transfer]);
message:要發送的數據信息
targetOrigin:接收消息的窗口的源(origin),即"協議 + 域名 + 端口"。也可以設爲*,表示不限制域名,向所有窗口發送。
*/


//****父窗口"http://aaa.com"向子窗口"http://bbb.com"發消息

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


//****子窗口通過message事件,監聽發送者的消息
window.addEventListener('message', function(event) {
  console.log(event.source);//發送源自的窗口:popup (子窗口可以通過event.source屬性引用父窗口,然後發送消息)
  console.log(event.origin);//發送源自的域:"http://aaa.com"(通過event.origin驗證發送者,分派事件的origin屬性的值不受調用窗口中document.domain的當前值的影響)
  console.log(event.data);//消息內容:'Hello World!'
},false);

安全問題

①如果您不希望從其他網站接收message,請不要爲message事件添加任何事件偵聽器這是一個完全萬無一失的方式來避免安全問題 

②如果您確實希望從其他網站接收message,請始終使用origin和source屬性驗證發件人的身份,以免收到惡意網站發送的惡意信息。 

③當您使用postMessage將數據發送到其他窗口時,始終指定精確的目標origin,而不是*,以免被惡意網站中間攔截postMessage發送的信息

詳情查看:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

2.AJAX

同源政策規定,AJAX請求只能發給同源的網址,否則就報錯。三種解決方案:

(1)架設服務器代理

瀏覽器請求同源服務器,再由後者請求外部服務

(2)JSONP

JSONP是服務器與客戶端跨源通信的常用方法,簡單適用,兼容性好。

基本原理:

①網頁添加一個<script>元素,向服務器請求一個腳本,直接作爲代碼運行,這不受同源政策限制,可以跨域請求。

②服務器收到請求後,拼接一個字符串,將 JSON 數據放在函數名裏面,作爲字符串返回(bar({...})

③客戶端會將服務器返回的字符串,作爲代碼解析,因爲瀏覽器認爲,這是<script>標籤請求的腳本內容。這時,客戶端只要定義了bar()函數,就能在該函數體內,拿到服務器返回的 JSON 數據。

//請求的腳本網址有一個callback參數(?callback=bar),用來告訴服務器,客戶端的回調函數名稱(bar)
<script src="http://api.foo.com?callback=bar"></script>


//定義bar()函數,在該函數體內,拿到服務器返回的 JSON 數據
function foo(data) {
  console.log('服務器返回:' + data.id);
};

//服務器收到這個請求以後,會將數據放在回調函數的參數位置返回。
foo({
  'ip': '8.8.8.8'
});

缺點:只能get請求

(3)WebSocket

WebSocket協議是一種基於TCP的網絡協議,取代用HTTP作爲傳輸層的雙向通訊技術——允許服務器主動發送信息給客戶端。使用ws://(非加密)和wss://(加密)作爲協議前綴。

①服務器根據WebSocket請求頭的Origin字段(表示:請求源自哪個域名),判斷是否許可本次通信

②如果該域名在白名單內,服務器就會做出迴應,所以沒有同源限制

//websocket請求頭(摘自網絡)

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

//判斷爲白名單後,服務端做出迴應

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

(3)CORS

CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫,是W3C標準,是跨源AJAX請求的根本解決方法。

  • 相比JSONP只能發GET請求,CORS允許任何類型的請求。
  • CORS 需要瀏覽器和服務器同時支持,實現 CORS 通信的關鍵是服務器。只要服務器實現了 CORS 接口,就可以跨域通信。
  • CORS 請求分成兩類,劃分的原因是,表單在歷史上一直可以跨域發出請求。簡單請求就是表單請求,瀏覽器沿襲了傳統的處理方式,不把行爲複雜化,否則開發者可能轉而使用表單,規避 CORS 的限制。對於非簡單請求,瀏覽器會採用新的處理方式:

①簡單請求:簡單的 HTTP 方法(head,get,post)與簡單的 HTTP 頭信息(Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type:只限於三個值application/x-www-form-urlencodedmultipart/form-datatext/plain)的結合。

  • 對於簡單請求,瀏覽器發現這次跨域 AJAX 請求是簡單請求,直接發出 CORS 請求,自動在頭信息之中,添加一個Origin字段。
  • 如果Origin指定的源,不在許可範圍內,服務器會返回一個正常的 HTTP 迴應,頭信息沒有包含Access-Control-Allow-Origin字段。瀏覽器收到後從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤無法通過狀態碼識別,因爲 HTTP 迴應的狀態碼有可能是200。
  • 如果Origin指定的域名在許可範圍內,服務端響應頭之中Access-Control-Allow-Origin字段包含請求頭中Origin字段的值。
//***請求頭:
GET /cors HTTP/1.1
Origin: http://api.bob.com  /*表示請求來自哪個域(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求*/
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...



//****如果Origin指定的域名在許可範圍內,服務端響應頭之中,會多出幾個頭信息字段,有三個與 CORS 請求相關的字段,都以Access-Control-開頭。
Access-Control-Allow-Origin: http://api.bob.com  /*該字段是必須的。它的值要麼是請求時Origin字段的值,要麼是一個*,表示接受任意域名的請求。同時,必須在 AJAX 請求中打開withCredentials屬性才起作用*/
Access-Control-Allow-Credentials: true/*可選,布爾值。設爲true,即表示服務器明確許可,瀏覽器可以把 Cookie 包含在請求中,一起發給服務器。*/
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

②非簡單請求:非簡單請求是那種對服務器提出特殊要求的請求,比如:請求方法是PUTDELETE,或者Content-Type字段的類型是application/json

  • 非簡單請求的 CORS 請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱爲“預檢”請求(preflight)。
  • 服務器收到“預檢”請求以後,檢查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以後,確認允許跨源請求,就可以做出迴應。
  • 如果服務器否定了“預檢”請求,會返回一個正常的 HTTP 迴應,但是沒有任何 CORS 相關的頭信息字段,或者明確表示請求不符合條件。
  • 一旦服務器通過了“預檢”請求,以後每次瀏覽器正常的 CORS 請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin頭信息字段。
//****JavaScript腳本:
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();



//****“預檢”請求頭
OPTIONS /cors HTTP/1.1  /*“預檢”請求用的請求方法是OPTIONS,表示這個請求是用來詢問的*/
Origin: http://api.bob.com  /*表示請求來自哪個源*/
Access-Control-Request-Method: PUT  /*該字段是必須的,用來列出瀏覽器的 CORS 請求會用到哪些 HTTP 方法,本例是PUT*/
Access-Control-Request-Headers: X-Custom-Header  /*該字段是一個逗號分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段,本例是X-Custom-Header*/
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...




①//****服務器否定了“預檢”請求的響應頭:
OPTIONS http://api.bob.com HTTP/1.1
Status: 200
Access-Control-Allow-Origin: https://notyourdomain.com/*明確不包括髮出請求的http://api.bob.com*/
Access-Control-Allow-Method: POST


//****瀏覽器發現服務器不同意預檢請求,觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲。控制檯報錯信息:
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.


②//****服務器允許了“預檢”請求的迴應頭:
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.bob.com  /*表示http://api.bob.com可以請求數據。該字段也可以設爲星號,表示同意任意跨源請求。*/
Access-Control-Allow-Methods: GET, POST, PUT  /*該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是爲了避免多次“預檢”請求。*/
Access-Control-Allow-Headers: X-Custom-Header  /*如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在“預檢”中請求的字段。*/
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
Access-Control-Max-Age: 1728000  /*該字段可選,用來指定本次預檢請求的有效期,單位爲秒。這裏有效期是20天(1728000秒),即允許緩存該條迴應1728000秒(即20天),在此期間,不用發出另一條預檢請求。*/


//****“預檢”請求通過之後,瀏覽器的會再發一個正常 CORS 請求:
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...



//****然後,服務器正常的迴應:
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

缺點:兼容性不好(>ie10)

具體參見:https://wangdoc.com/javascript/bom/cors.html

詳情查看:https://wangdoc.com/javascript/bom/same-origin.html

其他相關鏈接:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章