文章列出解決方案以及對應的demo, 拒絕說概念,不在稀裏糊塗。
什麼情況出現跨域?
- 協議不同
- 域名不同
- 端口不同
跨域解決方案
1.同一個主域下不同子域之間的跨域請求 - document.domain+iframe
同一個 origin 下,父頁面可以通過 iframe.contentWindow 直接訪問 iframe 的全局變量、DOM 樹等,iframe 可以也通過 parent/top 對父頁面做同樣的事情。
domain.html
<body>
<iframe id="ifr" src="http://b.tblog.com:3004/domain2.html"></iframe>
<script>
document.domain = 'tblog.com';
function aa(str) {
console.log(str);
}
window.onload = function () {
document.querySelector('#ifr').contentWindow.bb('aaa');
}
</script>
domain2.html
<body>
2222222222
<script>
document.domain = 'tblog.com';
function bb(str) {
console.log(str);
}
parent.aa('bbb');
</script>
</body>
完整demo
2. 完全不同源 - postMessage
html5新增API, 支持IE8+。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow 其他窗口的一個引用,比如iframe的contentWindow屬性、執行window.open返回的窗口對象、或者是命名過或數值索引的window.frames。
- message 將要發送到其他 window的數據
- targetOrigin 通過窗口的origin屬性來指定哪些窗口能接收到消息事件,其值可以是字符串""(表示無限制)或者一個URI。如果你明確的知道消息應該發送到哪個窗口,那麼請始終提供一個有確切值的targetOrigin,而不是。不提供確切的目標將導致數據泄露到任何對數據感興趣的惡意站點。
- transfer 可選 是一串和message 同時傳遞的 Transferable 對象. 這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。
傳遞過來的message的屬性有:
- data 從其他 window 中傳遞過來的對象。
- origin 調用 postMessage 時消息發送方窗口的 origin . 這個字符串由 協議、“://“、域名、“ : 端口號”拼接而成
- source 對發送消息的窗口對象的引用; 您可以使用此來在具有不同origin的兩個窗口之間建立雙向通信
下面index.html和index2.html通信
index.html
<body>
<input type="text" placeholder="http://b.tblog.com:3004/index2.html">
<iframe src="http://192.168.101.5: 3004/index2.html" frameborder="0"></iframe>
<script>
const input = document.querySelector('input');
input.addEventListener('input', function () {
window.frames[0].postMessage(this.value, '*');
// window.frames[0].postMessage(this.value, 'http://192.168.101.5');
// window.frames[0].postMessage(this.value, 'http://192.168.101.5:3004');
});
// 接收消息
window.addEventListener('message', function (e) {
input.value = e.data;
console.log('父窗口', e.data);
console.log('父窗口', e.source);
console.log('父窗口', e.origin);
});
</script>
</body>
index2.html
<body>
子窗口
<input id="input" type="text" placeholder="http://a.tblog.com:3004/index.html">
<script>
const input = document.querySelector('#input');
input.addEventListener('input', function () {
window.parent.postMessage(this.value, '*');
});
// 接收消息
window.addEventListener('message', function (e) {
input.value = e.data;
console.log('子窗口', e.data);
console.log('子窗口', e.source);
console.log('子窗口', e.origin);
});
</script>
</body>
完整demo
3. 完全不同源 - location.hash+iframe
原理是利用location.hash來進行傳值。改變hash並不會導致頁面刷新,所以可以利用hash值來進行數據傳遞,當然數據容量是有限的。
例如:假設a.tblog.com:3004 和 192.168.101.5:3004/index2.html
通信
原理:a.tblog.com:3004中index.html以iframe將192.168.101.5:3004/index2.html
頁面引入,在192.168.101.5:3004/index2.html
中插入新的iframe, 此iframe引入的頁面和a.tblog.com:3004同源,就可將192.168.101.5:3004/index2.html
的hash數據傳入a.tblog.com:3004頁面的hash值中。parent.parent.location.hash = self.location.hash.substring(1);
a.tblog.com:3004/index.html
<script>
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://192.168.101.5:3004/ index2.html#paramdo';
document.body.appendChild(ifr);
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is ' + data);
}
} catch (e) { };
}
setInterval(checkHash, 2000);
</script>
192.168.101.5:3004/ index2.html
<body>
<script>
//模擬一個簡單的參數處理操作
switch (location.hash) {
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}
function callBack() {
try {
parent.location.hash = 'somedata';
} catch (e) {
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://a.tblog.com:3004/index3.html#somedata'; // 注意該文件在"a.com"域下
document.body.appendChild(ifrproxy);
}
}
</script>
</body>
a.tblog.com:3004/index3.html
<body>
<script>
//因爲parent.parent和自身屬於同一個域,所以可以改變其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
</script>
</body>
完整demo
4. window.name + iframe 跨域
window.name 獲取/設置窗口的名稱。
窗口的名字主要用於爲超鏈接和表單設置目標(targets)。窗口不需要有名稱。
window.name屬性可設置或者返回存放窗口名稱的一個字符串, name值在不同頁面或者不同域下加載後依舊存在,沒有修改就不會發生變化,並且可以存儲非常長的name(2MB)。
場景1 - 同源
a.html
<body>
<script type="text/javascript">
const iframe = document.createElement('iframe');
iframe.src = 'http://a.tblog.com:3004/b.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function () {
console.log(iframe.contentWindow.name)
};
</script>
</body>
b.html
<body>
<script>
window.name = '子頁面的數據';
</script>
</body>
場景2 - 不同源
利用iframe中window.name在不同頁面或者不同域下加載後依舊存在的特性。a.tblog.com:3004/a.html
中通過iframe添加192.168.0.103:3004/b.html
(數據頁面, 指定window.name 的值),監聽iframe的load, 改變iframe的src與a.tblog.com:3004/a.html
同源代理頁面a.tblog.com:3004/c.html
(空頁面)。
a.tblog.com:3004/a.html
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;
iframe.onload = function () {
console.log('iframe.onload', state, iframe.contentWindow);
if (state === 1) {
const data = JSON.parse(iframe.contentWindow.name);
console.log(data, state);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if (state === 0) {
state = 1;
console.log('數據', window.name)
iframe.contentWindow.location = 'http://a.tblog.com:3004/c.html';
}
};
iframe.src = 'http://192.168.0.103:3004/b.html';
document.body.appendChild(iframe);
完整demo
5. 跨域jsonp
jsonp原理:
- 首先是利用script標籤的src屬性來實現跨域。
- 客戶端註冊callback方法名,攜帶在URL上, 如'http://127.0.0.1:8080/getNews?callback=getData'
- 服務器響應後生成json, 將json放在剛纔接收到的callback的函數中,就生成一段getData(json)
- 客戶端瀏覽器將script 標籤插入 DOM,解析script標籤後,會執行getData(json)。
由於使用script標籤的src屬性,因此只支持get方法
客戶端代碼
<body>
<button class="get">get data</button>
<script>
const btn = document.querySelector('.get');
btn.addEventListener('click', function () {
const script = document.createElement('script');
script.setAttribute('src', 'http://127.0.0.1:8080/getNews?callback=getData');
document.head.appendChild(script);
document.head.removeChild(script);
})
function getData(news) {
console.log(news)
}
</script>
</body>
服務端代碼
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
http.createServer(function(req, res){
const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/getNews':
const news = [{id: 678}];
res.setHeader('Content-type', 'text/json; charset=utf-8');
if(pathObj.query.callback){
res.end(pathObj.query.callback + '(' + JSON.stringify(news) + ')');
}else {
res.end(JSON.stringify(news));
}
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080);
完整demo
6. CORS跨域
原理
跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不同源服務器上的指定的資源。跨域資源共享( CORS )機制允許 Web 應用服務器進行跨域訪問控制,從而使跨域數據傳輸得以安全進行。
什麼情況下需要CORS
- 前文提到的由 XMLHttpRequest 或 Fetch 發起的跨域 HTTP 請求。
- Web 字體 (CSS 中通過 @font-face 使用跨域字體資源), 因此,網站就可以發佈 TrueType 字體資源,並只允許已授權網站進行跨站調用。
- WebGL 貼圖
- 使用 drawImage 將 Images/video 畫面繪製到 canvas
- 樣式表(使用 CSSOM)
功能概述
跨域資源共享標準新增了一組 HTTP 首部字段,允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源。允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源。對於get以外的請求,瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否允許該跨域請求。服務器確認允許之後,才發起實際的 HTTP 請求。 真個過程瀏覽器自動完成,服務器會添加一些附加的頭信息, 因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨源通信。
簡單請求
某些請求不會觸發 CORS 預檢請求。本文稱這樣的請求爲“簡單請求”,請注意,該術語並不屬於 Fetch (其中定義了 CORS)規範。只要同時滿足以下兩大條件,就屬於簡單請求:
(1) 請求方法是以下三種方法之一:
HEAD
GET
POST
(2)HTTP的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
請求響應結果多出的字段:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
- Access-Control-Allow-Origin
Access-Control-Allow-Origin: <origin> | *
; 該字段是必須的。它的值要麼是請求時Origin字段的值,要麼是一個*,表示接受任意域名的請求, 有次響應頭字段就可以跨域
- Access-Control-Allow-Credentials
Access-Control-Allow-Credentials: true
; 當瀏覽器的credentials設置爲true時, 此響應頭表示是否允許瀏覽器讀取response的內容,返回true則可以,其他值均不可以,Credentials可以是 cookies, authorization headers 或 TLS client certificates。
Access-Control-Allow-Credentials 頭 工作中與XMLHttpRequest.withCredentials 或Fetch API中的Request() 構造器中的credentials 選項結合使用。Credentials必須在前後端都被配置(即the Access-Control-Allow-Credentials header 和 XHR 或Fetch request中都要配置)才能使帶credentials的CORS請求成功。 如果withCredentials 爲false,服務器同意發送Cookie,瀏覽器也不會發送,或者,服務器要求設置Cookie,瀏覽器也不會處理。
需要注意的是,如果要發送Cookie,Access-Control-Allow-Origin就不能設爲星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie纔會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁代碼中的document.cookie也無法讀取服務器域名下的Cookie。
// 允許credentials:
Access-Control-Allow-Credentials: true
// 使用帶credentials的 XHR :
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
// 使用帶credentials的 Fetch :
fetch(url, {
credentials: 'include'
})
- Access-Control-Expose-Headers
在跨域訪問時,XMLHttpRequest對象的getResponseHeader()方法只能拿到一些最基本的響應頭,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma, 如果想拿到其他字段,就必須在Access-Control-Expose-Headers裏面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
代碼如下:
<!-- 服務端 -->
http.createServer(function(req, res){
const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
const news = [{id: 678}];
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// res.setHeader('Access-Control-Allow-Origin', '*');
// 需要cookie等憑證是必須
res.setHeader('Access-Control-Allow-Credentials', true);
res.end(JSON.stringify(news));
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080, (err) => {
if (!err) {
console.log('8080已啓動');
}
});
<!-- 客戶端 -->
<body>
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8080/user', true);
// 需要cookie等憑證是必須
xhr.withCredentials = true;
xhr.onreadystatechange = (e) => {
console.log('onreadystatechange', e)
}
xhr.send();
</script>
</body>
完整demo
非簡單請求
非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱爲"預檢"請求(preflight)。
以獲知服務器是否允許該實際請求。"預檢請求“的使用,可以避免跨域請求對服務器的用戶數據產生未預期的影響。
當請求滿足下述任一條件時,即應首先發送預檢請求:
-
使用了下面任一 HTTP 方法:
- put
- delete
- connect
- OPTIONS
- trace
- patch
-
人爲設置了對cors安全首部字段集合外的其他首部字段, 該集合爲:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-data
- Viewport-Width
- Width
-
Content-Type的值不屬於下列之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 請求中的XMLHttpRequestUpload 對象註冊了任意多個事件監聽器。
- 請求中使用了ReadableStream對象。
如下是一個需要執行預檢請求的 HTTP 請求:
<body>
<script>
const invocation = new XMLHttpRequest();
const url = 'http://localhost:8080/user';
const body = JSON.stringify({ name: 'toringo' });
function callOtherDomain() {
if (invocation) {
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/json');
invocation.onreadystatechange = (e) => {
console.log('onreadystatechange', e)
};
invocation.send(body);
}
}
callOtherDomain();
</script>
</body>
<!-- 服務端 -->
http.createServer(function(req, res){
const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
const news = {id: 678};
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// res.setHeader('Access-Control-Allow-Origin', '*');
// 需要cookie等憑證是必須
res.setHeader('Access-Control-Allow-Credentials', true);
res.end(JSON.stringify(news));
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080, (err) => {
if (!err) {
console.log('8080已啓動');
}
});
瀏覽器請求結果:
cors2.html:1 Access to XMLHttpRequest at 'http://localhost:8080/user' from origin 'http://127.0.0.1:3004' has been blocked by CORS policy: Request header field x-pingother is not allowed by Access-Control-Allow-Headers in preflight response.
如圖所示發起了預檢請求,請求頭部多了兩個字段:
Access-Control-Request-Method: POST; // 該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法.
Access-Control-Request-Headers: Content-Type, X-PINGOTHER; 告知服務器,實際請求將攜帶兩個自定義請求首部字段:X-PINGOTHER 與 Content-Type。服務器據此決定,該實際請求是否被允許。
上例需要成功響應數據,服務端需要同意:
http.createServer(function(req, res){
const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
const news = {id: 678};
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
// 新增的
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'X-PINGOTHER, Content-Type');
res.setHeader('Access-Control-Max-Age', 86400);
res.end(JSON.stringify(news));
break;
default:
res.writeHead(404, 'not found');
}
}).listen(8080, (err) => {
if (!err) {
console.log('8080已啓動');
}
});
服務段新增的字段:
Access-Control-Allow-Origin: req.headers.origin
Access-Control-Allow-Methods: POST, GET, OPTIONS // 表明服務器允許客戶端使用 POST, GET 和 OPTIONS 方法發起請求。該字段與 HTTP/1.1 Allow: response header 類似,但僅限於在需要訪問控制的場景中使用。這是爲了避免多次"預檢"請求。
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在"預檢"中請求的字段。
Access-Control-Max-Age: 86400 // 表明該響應的有效時間爲 86400 秒,也就是 24 小時。在有效時間內,瀏覽器無須爲同一請求再次發起預檢請求。請注意,瀏覽器自身維護了一個最大有效時間,如果該首部字段的值超過了最大有效時間,將不會生效。
7. nodejs代理跨域
node中間件實現跨域代理,是通過一個代理服務器,實現數據的轉發,也可以通過設置cookieDomainRewrite參數修改響應頭中cookie中域名,實現當前域的cookie寫入,方便接口登陸認證。
原理:服務器之間數據請求不存在跨域限制(同源策略是瀏覽器行爲), 所以先將請求代理到代理服務器, 代理服務器在內部請求真實的服務器得到結果後end連接。
<!-- 服務 -->
http.createServer(function(req, res){
const pathObj = url.parse(req.url, true);
console.log('server', pathObj.pathname)
switch(pathObj.pathname){
case '/user':
const news = {id: 678};
res.end(JSON.stringify(news));
break;
default:
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.end('未知錯誤');
}
}).listen(4000, (err) => {
if (!err) {
console.log('4000已啓動');
}
});
<!-- 代理 -->
http.createServer(function(req, res){
const pathObj = url.parse(req.url, true);
switch(pathObj.pathname){
case '/user':
res.setHeader('Content-type', 'text/json; charset=utf-8');
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'X-PINGOTHER, Content-Type',
});
console.log('proxy', req.method, pathObj.pathname);
// 請求真實服務器
const proxyRequest = http.request({
host: '127.0.0.1',
port: 4000,
url: '/',
path: pathObj.pathname,
method: req.method,
headers: req.headers
}, (proxyRes) => {
let body = '';
proxyRes.on('data', (chunk) => {
body += chunk;
});
proxyRes.on('end', () => {
console.log('響應的數據 ' + body );
res.end(body);
})
}).end();
break;
default:
res.writeHead(404, 'not found');
res.end(body);
break;
}
}).listen(8080, (err) => {
if (!err) {
console.log('8080已啓動');
}
});
<!-- 客戶端 index.html -->
<body>
<script>
const invocation = new XMLHttpRequest();
const url = 'http://localhost:8080/user';
const body = JSON.stringify({ name: 'toringo' });
function callOtherDomain() {
if (invocation) {
invocation.open('POST', url, true);
// invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/json');
invocation.onreadystatechange = (e) => {
console.log('onreadystatechange', e)
};
invocation.send(body);
}
}
callOtherDomain();
</script>
</body>
注意:
服務器和瀏覽器數據交互也需要遵循同源策略
-- 持續更新 --
Tips:
代碼地址。~ github
WeChat
參考文章
https://developer.mozilla.org...
http://vinc.top/2017/02/09/%E...
http://www.ruanyifeng.com/blo...
https://segmentfault.com/a/11...
https://developer.mozilla.org...
https://developer.mozilla.org...
http://www.ruanyifeng.com/blo...