一、什麼是跨域?
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" target="_blank" rel="external nofollow" >
<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 | 結果 | 原因 |
---|---|---|
| 成功 | 隻有路徑不同 |
| 成功 | 隻有路徑不同 |
| 失敗 | 不同協定 ( https和http ) |
| 失敗 | 不同端口 ( http:// 80是預設的) |
| 失敗 | 不同域名 ( 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隻能發
請求,CORS允許任何類型的請求。GET
- CORS 需要浏覽器和伺服器同時支援,實作 CORS 通信的關鍵是伺服器。隻要伺服器實作了 CORS 接口,就可以跨域通信。
- CORS 請求分成兩類,劃分的原因是,表單在曆史上一直可以跨域送出請求。簡單請求就是表單請求,浏覽器沿襲了傳統的處理方式,不把行為複雜化,否則開發者可能轉而使用表單,規避 CORS 的限制。對于非簡單請求,浏覽器會采用新的處理方式:
①簡單請求:簡單的 HTTP 方法(head,get,post)與簡單的 HTTP 頭資訊(Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type:隻限于三個值
application/x-www-form-urlencoded
、
multipart/form-data
、
text/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
②非簡單請求:非簡單請求是那種對伺服器提出特殊要求的請求,比如:請求方法是
PUT
或
DELETE
,或者
Content-Type
字段的類型是
application/json
。
- 非簡單請求的 CORS 請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱為“預檢”請求(preflight)。
- 伺服器收到“預檢”請求以後,檢查了
、Origin
和Access-Control-Request-Method
字段以後,确認允許跨源請求,就可以做出回應。Access-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