參考:
瀏覽器的同源策略
瀏覽器同源政策及其規(guī)避方法
同源政策
什么是同源策略?
同源策略限制了從同一個(gè)源加載的文檔或腳本如何與來自另一個(gè)源的資源進(jìn)行交互愚战。這是一個(gè)用于隔離潛在惡意文件的重要安全機(jī)制胞枕。
源的定義
如果兩個(gè)頁面的協(xié)議羹与,端口(如果有指定)和域名都相同,則兩個(gè)頁面具有相同的源爹橱。
同源檢測示例:http://www.example.com/directory/page.html
http://www.example.com/directory/other.html // 成功
http://www.example.com/directory/inner/another.html // 成功
https://example.com/index.html // 失敗 不同協(xié)議(http | https)
http://example.com:90/dir/secure.html // 失敗 不同端口(80 | 90)
http://new.example.com:90/dir/secure.html // 失敗 不同域名(new.example | example)
同源下的腳本只能讀取與所屬文檔同源的窗口和文檔的屬性萨螺。
同源政策的目的,是為了保護(hù)用戶信息的安全,防止惡意的網(wǎng)站竊取數(shù)據(jù)慰技。
目前椭盏,非同源將會受到限制的行為有:
- 無法讀取非同源資源下的:Cookie、LocalStorage吻商、IndexedDB掏颊。
- 無法操作非同源網(wǎng)頁的DOM。
- 無法向非同源地址發(fā)送Ajax請求(實(shí)際上艾帐,服務(wù)器會收到請求乌叶,也會返回,但最終被瀏覽器攔截)柒爸。
注意
- 對于當(dāng)前頁面來說頁面存放的 JS 文件的域不重要准浴,重要的是加載該 JS 頁面所在什么域。
- 同源策略限制的是腳本嵌入的文本來源揍鸟,而不是腳本本身兄裂。
不受同源策略限制:
- 頁面中的鏈接,重定向以及表單提交是不會受到同源策略限制的阳藻。
- 跨域資源的引入是可以的。但是js不能讀寫加載的內(nèi)容谈撒。如嵌入到頁面中的<script src="..."></script>腥泥,<img>,<link>啃匿,<iframe>等蛔外。
實(shí)現(xiàn)一個(gè)同源限制
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="./HS/css/test.css">
</head>
<body>
<h1>Hello World!</h1>
</body>
<script>
var xhr = new XMLHttpRequest();
xhr.open("GET","http://localhost:8080/getSomething",true);
xhr.send();
xhr.addEventListener("load",function() {
console.log(xhr.responseText);
});
</script>
</html>
server.js
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
var server = http.createServer(function (req, res) {
var pathObj = url.parse(req.url, true);
console.log(pathObj.pathname);
switch (pathObj.pathname) {
case "/getSomething":
res.end(
JSON.stringify({ beijing: "sunny" })
);
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function (err, data) {
if (err) {
res.writeHead(404, "not found");
res.end("<h1>404 Not Found</h1>")
} else {
res.end(data);
}
})
break;
}
});
server.listen(8080);
console.log("visit http://localhost:8080/httpServer.html");
HTML頁面中的Ajax請求就是test point,頁面中溯乒,我們設(shè)置了一個(gè)Ajax請求夹厌,并在node server中的switch語句進(jìn)行設(shè)定,當(dāng)請求發(fā)送后裆悄,如果有一個(gè)指向
http://localhost:8080/getSomething
的請求,本地服務(wù)器就會返回一個(gè)被JSON.stringify()方法解析后的JS對象。否則事镣,就使用fs模塊讀取靜態(tài)文件(css驳规、js等)
正常訪問成功下的請求狀況:
每一個(gè)文件都顯示
200 ok
,請求成功艾君,并且服務(wù)器也收到了Ajax請求采够,并返回?cái)?shù)據(jù)。
我們稍微對Ajax請求URL更改一下:
http://www.localhost:8080/getSomething
運(yùn)行服務(wù)器:
此時(shí)冰垄,我們就實(shí)現(xiàn)了一個(gè)跨域請求蹬癌,只是沒有數(shù)據(jù)返回。
我們發(fā)現(xiàn),HTTP狀態(tài)碼顯示200逝薪,說明請求是成功的隅要,但卻沒有數(shù)據(jù)返回,且有紅字報(bào)錯(cuò)翼闽。
Failed to load http://www.localhost:8080/getSomething: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.
出現(xiàn)這段提示拾徙,就是瀏覽器告訴你,跨域了感局!
No 'Access-Control-Allow-Origin' header is present on the requested resource
意思是不存在Access-Control-Allow-Origin
標(biāo)記尼啡,這個(gè)標(biāo)記是我們實(shí)現(xiàn)跨域時(shí)才會出現(xiàn)的,如果存在此標(biāo)記询微,瀏覽器不會對跨域的請求進(jìn)行攔截崖瞭,稍后會詳細(xì)解釋。
注意:請求是成功的撑毛,但是因?yàn)闉g覽器的安全機(jī)制书聚,請求的數(shù)據(jù)被攔截。
跨域 —— JSONP
JSONP是JSON with padding藻雌,填充式JSON或參數(shù)式JSON的縮寫雌续。是應(yīng)用JSON的一種新方法,在后來的Web服務(wù)器中非常流行胯杭。JSONP看起來與JSON差不多驯杜,只不過是被包含在函數(shù)調(diào)用中的JSON,就像下面這樣做个。
callback({"name": "Nicholas"});
JSONP由兩部分組成:
- 回調(diào)函數(shù)
- 數(shù)據(jù)
回調(diào)函數(shù)是當(dāng)響應(yīng)到來時(shí)應(yīng)該在頁面中調(diào)用的函數(shù)鸽心。回調(diào)函數(shù)的名字一般是在請求中指定的居暖。而數(shù)據(jù)就是傳入回調(diào)函數(shù)的JSON數(shù)據(jù)顽频。
http://freegeoip.net/json/?callback=handleResponse
這個(gè)URL請求在請求一個(gè)JSONP地理定位服務(wù)。通過查詢字符串來指定JSONP服務(wù)的回調(diào)函數(shù)是很常見的太闺,就像上面URL所示糯景,這里指定的回調(diào)函數(shù)名字叫handleResponse()
。
<script src="http://freegeoip.net/json/?callback=handleResponse"></script>
這個(gè)請求到達(dá)后端后跟束,后端回去解析callback這個(gè)參數(shù)獲取到字符串handleResponse莺奸,在發(fā)送數(shù)據(jù)做以下處理:
假設(shè)之前后端返回?cái)?shù)據(jù): {"city": "hangzhou", "weather": "晴天"}
,現(xiàn)在后端返回?cái)?shù)據(jù): handleResponse({"city": "hangzhou", "weather": "晴天"})
前端script標(biāo)簽在加載數(shù)據(jù)后會把 handleResponse({"city": "hangzhou", "weather": "晴天"})
做為 js 來執(zhí)行冀宴,這實(shí)際上就是調(diào)用handleResponse
這個(gè)函數(shù)灭贷,同時(shí)參數(shù)是{"city": "hangzhou", "weather": "晴天"}
。 用戶只需要在加載提前在頁面定義好handleResponse
這個(gè)全局函數(shù)略贮,在函數(shù)內(nèi)部處理參數(shù)即可甚疟。
<script>
function handlerResponse(ret){
console.log(ret);
}
</script>
<script src="http://freegeoip.net/json/?callback=handleResponse"></script>
總結(jié):
JSONP是通過 script 標(biāo)簽加載數(shù)據(jù)的方式去獲取數(shù)據(jù)當(dāng)做 JS 代碼來執(zhí)行 提前在頁面上聲明一個(gè)函數(shù)仗岖,函數(shù)名通過接口傳參的方式傳給后臺,后臺解析到函數(shù)名后在原始數(shù)據(jù)上「包裹」這個(gè)函數(shù)名览妖,發(fā)送給前端轧拄。換句話說,JSONP 需要對應(yīng)接口的后端的配合才能實(shí)現(xiàn)讽膏。
栗子
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="container">
<ul class="news">
</ul>
<button class="show">show news</button>
</div>
</body>
<script>
$('.show').addEventListener('click', function () {
var script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/getNews?callback=handleResponse';
document.head.appendChild(script);
document.head.removeChild(script);
})
function handleResponse(news) {
var html = '';
for (var i = 0; i < news.length; i++) {
html += '<li>' + news[i] + '</li>';
}
console.log(html);
$('.news').innerHTML = html;
}
function $(id) {
return document.querySelector(id);
}
</script>
</html>
我們創(chuàng)建了一個(gè)<script>節(jié)點(diǎn)檩电,并向其src屬性賦值為一個(gè)跨域URL的Ajax請求(見node server)。在document文檔頭部添加了這個(gè)節(jié)點(diǎn)府树,當(dāng)文檔加載運(yùn)行時(shí)俐末,一旦文檔樹中有引用script標(biāo)簽,都會將其下載下來奄侠,所以屆時(shí)會自動下載script腳本卓箫,只不過這個(gè)腳本的本質(zhì)是JSON。removeChild是為了加載script后在移除它垄潮,保持整體語義烹卒。
接下來定義callback,這里回調(diào)函數(shù)的作用就是接受JSON字符串弯洗,作為參數(shù)進(jìn)入函數(shù)轉(zhuǎn)化為字符串旅急,進(jìn)而轉(zhuǎn)換為HTML元素內(nèi)容等等。而為其“包裹”上回調(diào)函數(shù)的行為牡整,是在后端進(jìn)行的
node server
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
var server = http.createServer(function (req, res) {
var pathObj = url.parse(req.url, true);
switch (pathObj.pathname) {
case "/getNews":
var news = ["News-A", "News-B", "News-C"];
res.setHeader("Content-Type", "text/json; charset=utf-8");
console.log(pathObj); // '/getNews?callback=handleResponse'
console.log(pathObj.query.callback); // handleResponse
if (pathObj.query.callback) {
res.end(pathObj.query.callback + "(" + JSON.stringify(news) + ")");
// handleResponse(["News-A","News-B","News-C"]) 服務(wù)端返回的數(shù)據(jù)
} else {
res.end(JSON.stringify(news));
}
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function (e, data) {
if (e) {
res.writeHead(404, "not found");
res.end("<h1>404 Fot Found</h1>")
} else {
res.end(data);
}
})
}
});
server.listen(8080);
console.log("visit http://localhost:8080/httpServer.html");
node代碼如上坠非,我們這里模擬的JSON是一個(gè)新聞列表。模擬了一個(gè)/getNews/
URL果正,使用script作為JS腳本下載并執(zhí)行。
如果存在一個(gè)回調(diào)"值"盟迟,就把此值當(dāng)做作為JS函數(shù)名秋泳,并將JSON字符串傳遞進(jìn)來。
點(diǎn)擊后攒菠,我們就看到了一個(gè)來自跨域的HTML內(nèi)容迫皱。
JSONP之所以在開發(fā)人員中極為流行,主要原因是它非常簡單易用辖众。與圖像Ping相比卓起,它的優(yōu)點(diǎn)在于能夠直接訪問響應(yīng)文本,支持在瀏覽器與服務(wù)器之間雙向通信凹炸。不過戏阅,JSONP也有兩點(diǎn)不足:
- 首先JSONP是從其他域中加載代碼執(zhí)行,如果其他域不安全啤它,很可能會在響應(yīng)中夾帶一些惡意代碼奕筐,而此時(shí)除了完全放棄JSONP調(diào)用之外舱痘,沒有辦法追究。因此在使用不是你自己運(yùn)維的Web服務(wù)時(shí)离赫,一定保證安全可靠芭逝。
- 確定JSONP請求是否失敗不容易
————《JS高程》
跨域 —— CORS
通過XHR實(shí)現(xiàn)Ajax通信的一個(gè)主要限制,來源于跨域安全策略渊胸。默認(rèn)情況下旬盯,XHR對象只能訪問包含它的頁面位于同一個(gè)域中的資源。這種安全策略可以預(yù)防某些惡意行為翎猛。但是胖翰,實(shí)現(xiàn)合理的跨域請求對開發(fā)某些瀏覽器應(yīng)用程序也是直觀重要的。
CORS(Cross-Origin Resource Sharing办成,跨域資源共享)是W3C的一個(gè)工作草案泡态,定義了在必須訪問跨域資源時(shí),瀏覽器與服務(wù)器應(yīng)該如何溝通迂卢。CORS背后思想某弦,就是使用自定義的HTTP頭部讓瀏覽器與服務(wù)器進(jìn)行溝通,從而決定請求或響應(yīng)是應(yīng)該成功而克,還是應(yīng)該失敗靶壮。
比如一個(gè)簡單的使用GET和POST發(fā)送的請求,它沒有自定義的頭部员萍,而主體內(nèi)容是text/plain腾降。在發(fā)送該請求時(shí),需要給它附加一個(gè)額外的Origin頭部碎绎,其中包含請求頁面的源信息(協(xié)議螃壤、域名和端口),以便服務(wù)器根據(jù)這個(gè)頭部信息來決定是否給予響應(yīng)筋帖。
總結(jié):使用 XMLHttpRequest 發(fā)送請求時(shí)奸晴,瀏覽器發(fā)現(xiàn)該請求不符合同源策略,會給該請求加一個(gè)請求頭:Origin日麸,后臺進(jìn)行一系列處理寄啼,如果確定接受請求則在返回結(jié)果中加入一個(gè)響應(yīng)頭:Access-Control-Allow-Origin; 瀏覽器判斷該相應(yīng)頭中是否包含 Origin 的值,如果有則瀏覽器會處理響應(yīng)代箭,我們就可以拿到響應(yīng)數(shù)據(jù)墩划,如果不包含瀏覽器直接駁回,這時(shí)我們無法拿到響應(yīng)數(shù)據(jù)嗡综。
栗子
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="container">
<ul class="news">
</ul>
<button class="show">show news</button>
</div>
</body>
<script>
$('.show').addEventListener('click', function () {
var xhr = new XMLHttpRequest()
xhr.open('GET', 'http://127.0.0.1:8080/getNews', true)
xhr.send();
xhr.onload = function () {
appendHtml(JSON.parse(xhr.responseText))
}
})
function appendHtml(news) {
var html = '';
for (var i = 0; i < news.length; i++) {
html += '<li>' + news[i] + '</li>';
}
$('.news').innerHTML = html;
}
function $(selector) {
return document.querySelector(selector)
}
</script>
</html>
node server
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
var server = http.createServer(function (req, res) {
var pathObj = url.parse(req.url, true);
switch (pathObj.pathname) {
case "/getNews":
var news = ["News-A", "News-B", "News-C"];
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080');
res.end(JSON.stringify(news));
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function (e, data) {
if (e) {
res.writeHead(404, "not found");
res.end("<h1>404 Fot Found</h1>")
} else {
res.end(data);
}
})
}
});
server.listen(8080);
console.log("visit http://localhost:8080/httpServer.html");
HTML中發(fā)送Ajax請求后http://127.0.0.1:8080/getNews
乙帮,會在請求頭部自動加上Origin。
之后在后端設(shè)置了一個(gè)
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
蛤高,表示接受來自http://localhost:8080的請求蚣旱,在響應(yīng)頭部中打上了Access-Control-Allow-Origin
標(biāo)記碑幅,返回?cái)?shù)據(jù)后,瀏覽器會識別標(biāo)記塞绿,確認(rèn)通過拿到數(shù)據(jù)沟涨。
我們修改一下,假設(shè)后端只接收端口9000的跨域請求异吻,那么會這樣裹赴。
服務(wù)器一樣會返回請求,在Preview中我們可以看到诀浪,但是請求域的端口不一樣棋返,于是被瀏覽器攔截了。
如若想通過任何域的請求雷猪,可以這樣設(shè)置:
res.setHeader('Access-Control-Allow-Origin','*');