1. 什么是同源策略
瀏覽器限制不同源的兩個網(wǎng)站間腳本和文本的相互訪問,只允許訪問同源下的內(nèi)容。所謂同源,就是指兩個頁面具有相同的協(xié)議,主機(也常說域名),端口,三個要素缺一不可。同源政策的目的,是為了保證用戶信息的安全,防止惡意的網(wǎng)站竊取數(shù)據(jù)。
2. 什么是跨域?跨域有幾種實現(xiàn)形式
- 關(guān)于跨域
說白點就是post、get的url不是你當前的網(wǎng)站,域名不同。例如在aaa.com/a.html里面,表單的提交action是bbb.com/b.html。
不僅如此www.aaa.com和aaa.com之間也屬于跨域,因為www.aaa.com是二級域名,aaa.com是根域名。
JavaScript出于安全方面的考慮,是不允許跨域調(diào)用其他頁面的對象的
[圖片上傳失敗...(image-b1caef-1514307447090)]
[圖片上傳失敗...(image-50a6ca-1514307447091)] - 關(guān)于跨域的原因
跨域這東西其實很常見,例如我們可以把網(wǎng)站的一些腳本、圖片或其他資源放到另外一個站點。例如我們可以使用Google提供的jQuery,加載時間少了,而且減少了服務(wù)器的流量,如下
<script type="text/java script"src="https://aja x.googleapis.com/aj ax/libs/jquery/1.4.2/jquery.min.js"></script>
有時候不僅僅是一些腳本、圖片這樣的資源,我們也會希望從另外的站點調(diào)用一些數(shù)據(jù)(有時候是不得不這樣),例如我希望獲取一些blog的RSS來生成一些內(nèi)容,再或者說我在“人人開放平臺”上開發(fā)一個應(yīng)用,需要調(diào)用人人的數(shù)據(jù)。
然而,很不幸的是,直接用XMLHttpRequest來Get或者Post是不行的。
- 跨域的實現(xiàn)方式
- 在服務(wù)器端進行通信,因為服務(wù)器端不受跨域的限制??缬虻陌踩拗剖菍g覽器端來說的。
- 用 script 標簽來解決,因為它的 src 屬性是可以跨域的。
eg. 往常用 script 引入外部的 js
eg. 也可以用 js 動態(tài)創(chuàng)建 script 標簽,并將其 append 到 body 里,就可以用這個 js 了。- jsonp:它其實是一個非官方的協(xié)議,有特定的通信邏輯,但平時原生用的不多,因為 jQuery 本身就支持了 jsonp。
-
$.ajax()
,$.getJSON():jQuery
提供的現(xiàn)成函數(shù)來實現(xiàn)跨域。這兩種方式(其實就是一種調(diào)用方式,需要傳特定的參數(shù))的原理是將 json 數(shù)據(jù)填充進回調(diào)函數(shù),即創(chuàng)建一個回調(diào)函數(shù),在遠程服務(wù)器上調(diào)用這個函數(shù)并將 json 數(shù)據(jù)形式作為參數(shù),完成回調(diào)。 - iFrame 實現(xiàn)跨域
- HTML5 的 postMessage 和 onmessage
其實 jsonp 里面也是創(chuàng)建了 script 標簽,然后通過 js 回調(diào)的形式實現(xiàn)了跨域訪問。而 jQuery 對 jsonp 也僅僅是支持,其核心也是 jsonp。所以,歸根結(jié)底,這三種方式都是常見又經(jīng)常被忽略的 script 標簽。
3. JSONP 的原理是什么
jsonp是一種跨域通信的手段,它的原理其實很簡單:
首先是利用script標簽的src屬性來實現(xiàn)跨域。
通過將前端方法作為參數(shù)傳遞到服務(wù)器端,然后由服務(wù)器端注入?yún)?shù)之后再返回,實現(xiàn)服務(wù)器端向客戶端通信。
由于使用script標簽的src屬性,因此只支持get方法
4. CORS是什么
CORS 全稱是跨域資源共享(Cross-Origin Resource Sharing),是一種 ajax 跨域請求資源的方式,支持現(xiàn)代瀏覽器,IE支持10以上。 實現(xiàn)方式很簡單,當你使用 XMLHttpRequest 發(fā)送請求時,瀏覽器發(fā)現(xiàn)該請求不符合同源策略,會給該請求加一個請求頭:Origin,后臺進行一系列處理,如果確定接受請求則在返回結(jié)果中加入一個響應(yīng)頭:Access-Control-Allow-Origin; 瀏覽器判斷該相應(yīng)頭中是否包含 Origin 的值,如果有則瀏覽器會處理響應(yīng),我們就可以拿到響應(yīng)數(shù)據(jù),如果不包含瀏覽器直接駁回,這時我們無法拿到響應(yīng)數(shù)據(jù)。所以 CORS 的表象是讓你覺得它與同源的 ajax 請求沒啥區(qū)別,代碼完全一樣。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發(fā)者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
實現(xiàn)CORS通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實現(xiàn)了CORS接口,就可以跨源通信。
瀏覽器將CORS請求分為兩類:簡單請求和非簡單請求
5. 演示三種以上跨域的解決方式
1.JSONP實現(xiàn)跨域
Web 頁面上調(diào)用 js 文件不受瀏覽器同源策略的影響,所以通過 Script 便簽可以進行跨域的請求:
- 首先前端先設(shè)置好回調(diào)函數(shù),并將其作為 url 的參數(shù)。
- 服務(wù)端接收到請求后,通過該參數(shù)獲得回調(diào)函數(shù)名,并將數(shù)據(jù)放在參數(shù)中將其返回
- 收到結(jié)果后因為是 script 標簽,所以瀏覽器會當做是腳本進行運行,從而達到跨域獲取數(shù)據(jù)的目的。
實例演示:
后端邏輯:文件名server.js
//后端邏輯
var url = require('url')
var http = require('http')
http.createServer(function (req, res) {
var data = {
name: "haha"
};
var callback = url.parse(req.url,true).query.callback;
//url.parse(req.url,true).query即{callback:jsonpCallback} ----------------獲取函數(shù)名callback
res.writeHead(200)
res.end(`${callback}(${JSON.stringify(data)})`)
}).listen(3000, '127.0.0.1')
//服務(wù)端接收到請求后獲取到回調(diào)函數(shù)名callback,
將數(shù)據(jù)放在參數(shù)中將其返回,即返回callback({"name" : "haha"}) 函數(shù)調(diào)用
console.log('監(jiān)聽127.0.0.1:3000端口')
//注意這里文字一定要引號包裹不然會報錯
//注意終端提示:SyntaxError: Invalid or unexpected token 無效或意外的標記,
出現(xiàn)這種情況一般是中文的標點符號或者漏寫了符號導(dǎo)致的
前端頁面:文件名index.html
<!--前端頁面-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP實現(xiàn)跨域</title>
</head>
<body>
<script>
function jsonpCallback(data) {
console.log(JSON.stringfy(data))
}
//定義回調(diào)函數(shù)jsonpCallback,收到服務(wù)端返回結(jié)果
callback({"name" : "haha"}),callback=jsonpCallback
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
//web頁面上調(diào)用js文件不收同源策略影響
</body>
</html>
驗證過程:
-
終端在后端文件server.js目錄下,node server.js啟動服務(wù),監(jiān)聽端口3000
-
我們通過端口號的不同來模擬跨域的場景,通過127.0.0.1:8080端口來訪問頁面.
這里我們打開另一個終端,在頁面同目錄下輸入http-server啟動端口
這樣就可以通過端口 8080 訪問 index.html 剛才那個頁面了,相當于是開啟兩個監(jiān)聽不同端口的 http 服務(wù)器,通過頁面中的請求來模擬跨域的場景。打開瀏覽器,訪問 http://127.0.0.1:8080就可以看到從http://127.0.0.1:3000獲取到的數(shù)據(jù)了。
至此,通過 JSONP 跨域獲取數(shù)據(jù)已經(jīng)成功了,但是通過這種事方式也存在著一定的優(yōu)缺點:
優(yōu)點:
1.它不像XMLHttpRequest 對象實現(xiàn) Ajax 請求那樣受到同源策略的限制
2.兼容性很好,在古老的瀏覽器也能很好的運行
3.不需要 XMLHttpRequest 或 ActiveX 的支持;并且在請求完畢后可以通過調(diào)用 callback 的方式回傳結(jié)果。
缺點:
1.它支持 GET 請求而不支持 POST 等其它類型的 HTTP 請求。
2.它只支持跨域 HTTP 請求這種情況,不能解決不同域的兩個頁面或 iframe 之間進行數(shù)據(jù)通信的問題
3.容易遭受XSS攻擊,因為我們拿到的是對方接口的數(shù)據(jù)作為js執(zhí)行,如果得到的是一個很危險js,獲取了用戶信息和cookies,這時執(zhí)行了js就會出現(xiàn)安全問題。
2. CORS實現(xiàn)跨域
CORS 是一個 W3C 標準,全稱是"跨域資源共享"(Cross-origin resource sharing)它允許瀏覽器向跨源服務(wù)器,發(fā)出 XMLHttpRequest 請求,從而克服了 ajax 只能同源使用的限制。
CORS 需要瀏覽器和服務(wù)器同時支持才可以生效,對于開發(fā)者來說,CORS 通信與同源的 ajax 通信沒有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn) ajax 請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。參考阮一峰
因此,實現(xiàn) CORS 通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實現(xiàn)了 CORS 接口,就可以跨源通信。
<!--前端頁面-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var xhr = new XMLHttpRequest()
xhr.open('GET', 'http://127.0.0.1:3000', true)
xhr.send()
xhr.onload = function () {
console.log(xhr.responseText)
}
</script>
</body>
</html>
這似乎跟一次正常的異步 ajax 請求沒有什么區(qū)別,關(guān)鍵是在服務(wù)端收到請求后的處理:
var http = require('http')
http.createServer(function(req,res){
res.writeHead(200,{
'Acess-Control-Allow-Origin':'http://127.0.0.1:8080'
})
res.end('這是你要的數(shù)據(jù): haha')
}).listen(3000,'127.0.0.1')
console.log('監(jiān)聽127.0.0.1:3000端口')
關(guān)鍵是在于設(shè)置相應(yīng)頭中的 Access-Control-Allow-Origin,該值要與請求頭中 Origin 一致才能生效,否則將跨域失敗。
開啟兩個http服務(wù)器
打開瀏覽器訪問localhost:8080,
成功的關(guān)鍵在于 Access-Control-Allow-Origin是否包含請求頁面的域名,如果不包含的話,瀏覽器認為是跨域請求,會攔截返回的信息.
另外如果服務(wù)器設(shè)置:
res.writeHead(200,{
'Acess-Control-Allow-Origin': * //開放接口,任何人都能跨域調(diào)用該接口內(nèi)的數(shù)據(jù)
})
CORS 的優(yōu)缺點:
優(yōu)點:
- 使用簡單方便,更為安全
- 支持 POST 請求方式
- 精確控制資源訪問權(quán)限
- 客戶端無需增加額外代碼
缺點:
1. CORS僅兼容 IE 10 以上
3.Server Proxy服務(wù)器代理
需要跨域的請求操作時發(fā)送請求給后端,讓后端幫你代為請求,然后將獲取的結(jié)果發(fā)送給你。
假設(shè)你的頁面需要獲取 CNode:Node.js專業(yè)中文社區(qū) 論壇上一些數(shù)據(jù),如通過 https://cnodejs.org/api/v1/topics,因為不同域,所以你可以請求后端讓其代為轉(zhuǎn)發(fā)請求。
var url = require('url');
var http = require('http');
var https = require('https');
var server = http.createServer((req, res) => { // =>前面是參數(shù),后面是函數(shù)體
var path = url.parse(req.url).path.slice(1);
if(path === 'topics'){
https.get('https://cnodejs.org/api/v1/topics',(resp) => {
var data = "";
resp.on('data',chunk => {
data += chunk;
})
resp.on('end', () => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(data);
})
})
}
}).listen(3000,'127.0.0.1');
console.log('啟動服務(wù),監(jiān)聽 127.0.0.1:3000');
開啟服務(wù)器:
打開瀏覽器訪問http://localhost:3000/topics就可以看到:
獲取到數(shù)據(jù),跨域成功
總結(jié)
四種iframe跨域
1.postMessage實現(xiàn)跨域
postMessage 是 HTML5 新增加的一項功能,跨文檔消息傳輸(Cross Document Messaging),目前:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持這項功能,使用起來也特別簡單。
首先創(chuàng)建 a.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
<script>
window.onload = function(){
var targetOrigin = 'http://localhost:8081';
window.frames[0].postMessage('看到我發(fā)給你的信息沒',targetOrigin) //window.frames[0]是內(nèi)嵌的窗口
}
window.addEventListener('message',function(e){
console.log('a.html接收到信息:',e.data)
});
</script>
</body>
</html>
創(chuàng)建內(nèi)嵌b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b.html</title>
</head>
<body>
<script>
window.addEventListener('message',function(e){
if(e.source != window.parent){
return;
}
var data = e.data;
console.log('b.html 接收到的消息:', data);
parent.postMessage('我看到你發(fā)的消息了!',e.origin)
})
</script>
</body>
</html>
然后同時開啟服務(wù)器:注意b.html服務(wù)器開啟更改接口命令:http-server -p 8081
在瀏覽器中打開http://127.0.0.1:8080/a.html
跨域成功.
參考教程:Window.postMessage()
2.document.domain實現(xiàn)跨域:(降域)
對于主域相同而子域不同的情況下,可以通過設(shè)置 document.domain 的辦法來解決,具體做法是可以在 http://www.example.com/a.html
和http://sub.example.com/b.html
兩個文件分別加上 document.domain = "a.com";然后通過 a.html 文件創(chuàng)建一個 iframe,去控制 iframe 的 window,從而進行交互,當然這種方法只能解決主域相同而二級域名不同的情況
測試的方式稍微復(fù)雜點,需要安裝 nginx 做域名映射,如果你電腦沒有安裝 nginx,請先去安裝一下: nginx news
安裝教程:https://www.cnblogs.com/chuncn/archive/2011/10/14/2212291.html
先創(chuàng)建一個 a.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<script>
document.domain = 'example.com' //a,b分別加上 document.domain = "example.com"
var ifr = document.createElement('iframe'); //創(chuàng)建一個iframe 內(nèi)嵌窗
ifr.src = 'http://sub.example.com/b.html';
ifr.style.display = 'none';
document.body.append(ifr)
ifr.onload = function(){
var win = ifr.contentWindow; //控制內(nèi)嵌iframe的窗口信息
alert(win.data)
}
</script>
</body>
</html>
創(chuàng)建b.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b.html</title>
</head>
<body>
<script>
document.domain = 'example.com';
window.data = '傳送的數(shù)據(jù):1111';
</script>
</body>
</html>
開啟服務(wù)器
這時候只是開啟了兩個 http 服務(wù)器,還需要通過 nginx 做域名映射,將 Example Domain映射到 localhost:8080,sub.example.com 映射到localhost:8081上打開操作系統(tǒng)下的 hosts 文件:mac 是位于 /etc/hosts 文件,并添加:
127.0.0.1 www.example.com
127.0.0.1 sub.example.com
windows修改host參考:
http://chongzhuang.windowszj.com/faq/164.html
修改好后保存,這樣在瀏覽器打開兩個網(wǎng)址后就會訪問本地的服務(wù)器.
之后打開 nginx 的配置文件:/usr/local/etc/nginx/nginx.conf
,并在 http 模塊里添加:
server {
listen 80;
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:8080/;
}
}
server {
listen 80;
server_name sub.example.com;
location / {
proxy_pass http://127.0.0.1:8081/;
}
}
如果訪問本地的域名是www.example.com,就由 localhost:8080 代理該請求。所以我們這時候在打開瀏覽器訪問www.example.com的時候其實訪問的就是本地服務(wù)器 localhost:8080。
3.location.hash實現(xiàn)跨域
在 url 中,http://www.baidu.com#helloworld的#helloworld 就是 location.hash,改變 hash 值不會導(dǎo)致頁面刷新,所以可以利用 hash 值來進行數(shù)據(jù)的傳遞,當然數(shù)據(jù)量是有限的。
假設(shè)localhost:8080下有文件 cs1.html要和 localhost:8081 下的 cs2.html傳遞消息,cs1.html首先創(chuàng)建一個隱藏的iframe,iframe 的 src指向 localhost:8081/cs2.html,這時的 hash 值就可以做參數(shù)傳遞。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CS1</title>
</head>
<body>
<script>
// http://localhost:8080/cs1.html
let ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = "http://localhost:8081/cs2.html#data";
document.body.appendChild(ifr);
function checkHash() {
try {
let data = location.hash ? location.hash.substring(1) : '';
console.log('獲得到的數(shù)據(jù)是:', data);
}catch(e) {
}
}
window.addEventListener('hashchange', function(e) {
console.log('獲得的數(shù)據(jù)是:', location.hash.substring(1));
});
</script>
</body>
</html>
cs2.html 收到消息后通過 parent.location.hash 值來修改 cs1.html 的 hash 值,從而達到數(shù)據(jù)傳遞。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CS2</title>
</head>
<body>
<script>
// http://locahost:8081/cs2.html
switch(location.hash) {
case "#data":
callback();
break;
}
function callback() {
const data = "some number: 1111"
try {
parent.location.hash = data;
}catch(e) {
// ie, chrome 下的安全機制無法修改 parent.location.hash
// 所以要利用一個中間的代理 iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://localhost:8080/cs3.html#' + data; // 該文件在請求域名的域下
document.body.appendChild(ifrproxy);
}
}
</script>
</body>
</html>
作者:FLYSASA
鏈接:http://www.lxweimin.com/p/211cace53f04
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CS2</title>
</head>
<body>
<script>
// http://locahost:8081/cs2.html
switch(location.hash) {
case "#data":
callback();
break;
}
function callback() {
const data = "some number: 1111"
try {
parent.location.hash = data;
}catch(e) {
// ie, chrome 下的安全機制無法修改 parent.location.hash
// 所以要利用一個中間的代理 iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://localhost:8080/cs3.html#' + data; // 該文件在請求域名的域下
document.body.appendChild(ifrproxy);
}
}
</script>
</body>
</html>
由于兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要借助于 localhost:8080 域名下的一個代理 iframe 的 cs3.html 頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cs3</title>
</head>
<body>
<script>
parent.parent.location.hash = self.location.hash.substring(1);
</script>
</body>
</html>
同樣開啟服務(wù)器
這里為了圖方便,將 cs1,2,3 都放在同個文件夾下,實際情況的話 cs1.html 和 cs3.html 要與 cs2.html 分別放在不同的服務(wù)器才對。
之后打開瀏覽器訪問 localhost:8080/cs1.html,注意不是 8081,就可以看到獲取到的數(shù)據(jù)了,此時頁面的 hash 值也已經(jīng)改變。
缺點:
- 數(shù)據(jù)直接暴露在了 url 中
- 數(shù)據(jù)容量和類型都有限
4.window.name實現(xiàn)跨域
window.name(一般在 js 代碼里出現(xiàn))的值不是一個普通的全局變量,而是當前窗口的名字,這里要注意的是每個 iframe 都有包裹它的window,而這個 window 是top window 的子窗口,而它自然也有 window.name 的屬性,window.name 屬性的神奇之處在于 name 值在不同的頁面(甚至不同域名)加載后依舊存在(如果沒修改則值不會變化),并且可以支持非常長的 name 值(2MB)。
舉個簡單的例子:
你在某個頁面的控制臺輸入:
window.name = "Hello World";
window.location = "http://www.baidu.com";
頁面跳轉(zhuǎn)到了百度首頁,但是window.name卻被保存了下來,還是 Hello World,跨域解決方案似乎可以呼之欲出了:
頁面跳轉(zhuǎn)到了百度首頁,但是window.name卻被保存了下來,還是 Hello World,跨域解決方案似乎可以呼之欲出了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<script>
var data = '';
const ifr = document.createElement('iframe');
ifr.src = "http://localhost:8081/b.html";
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function() {
ifr.onload = function() {
data = ifr.contentWindow.name;
console.log('收到數(shù)據(jù):', data);
}
ifr.src = "about:blank";
}
</script>
</body>
</html>
再創(chuàng)建b文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b.html</title>
</head>
<body>
<script>
window.name = "你想要的數(shù)據(jù)!";
</script>
</body>
</html>
http://localhost:8080/a.html在請求遠端服務(wù)器http://localhost:8081/b.html的數(shù)據(jù),我們可以在該頁面下新建一個 iframe,該iframe 的 src 屬性指向服務(wù)器地址(利用 iframe 標簽的跨域能力),服務(wù)器文件 b.html 設(shè)置好 window.name 的值。
但是由于 a.html頁面和該頁面iframe 的 src 如果不同源的話,則無法操作iframe里的任何東西,所以就取不到 iframe 的 name值,所以我們需要在b.html加載完后重新?lián)Q個src設(shè)置成'about:blank;' ,如果不重新指向 src的話直接獲取 window.name 的話會獲取不到數(shù)據(jù).
在a.html中重新設(shè)置ifr.src = "about:blank";后,打開兩個http服務(wù)器,訪問http://127.0.0.1:8080/a.html