瀏覽器的同源策略限制了 從一個源加載的文檔或腳本與來自另外一個源的資源的交互。它是隔離潛在惡意文檔的關鍵安全機制。php
具體限制:css
若是兩個頁面具備相同的協議、域名和端口(若是有指定),則這兩個頁面具備相同的源。html
Tips:http協議默認端口是80,https默認端口是443。前端
腳本能夠將 document.domain的值設置爲當前域或當前域的父域。若是設置爲超級域,那麼超級域將用於後續的源檢查。html5
Eg:對頁面 http://store.company.com/dir/other.html 進行域的修改:git
document.domain = 'company.com';
該js執行後,該頁面將會成功地經過對http://company.com/dir/page.html的同源檢測。github
一般容許跨源資源嵌入(Cross-origin embedding)。web
有如下這些狀況:ajax
<script src="..."> </script>
<link rel="stylesheet" href="...">
css跨域須要設置一個正確的Content-Type消息頭。不一樣瀏覽器有不一樣限制。通常均可以成功跨源獲取css資源。json
一些瀏覽器容許跨域字體( cross-origin fonts),一些須要同源字體(same-origin fonts)。
iframe自己就是能夠跨域的。
站點可使用 X-Frame-Origins消息頭來阻止這種跨域。
X-Frame-Origins,是一個HTTP響應頭,用來指示瀏覽器是否容許一個頁面能夠在iframe/frame/object中展現。網站可使用此功能,來確保本身網站的內容沒有被嵌到別人的網站中去。
可能值:
<iframe width="1000" height="800" src="https://www.baidu.com"></iframe>
該頁面能夠正確地嵌入百度首頁。可是控制檯會報以下錯誤信息:
Uncaught DOMException: Blocked a frame with origin "https://www.baidu.com" from accessing a cross-origin frame at HTMLDocument.t...
能夠發現,這些跨域錯誤信息都是因爲須要進行js交互纔出現的。
<iframe width="1000" height="800" src="http://www.ftchinese.com"></iframe>
iframe區域展示的是空白。而後console控制檯報錯的信息爲:
Refused to display 'http://www.ftchinese.com/' in a frame because it set 'X-Frame-Options' to 'deny'.
一般不容許跨域讀操做(Cross-origin reads)。通常不能經過ajax的方法去請求不一樣源的資源。 瀏覽器中不一樣域的框架之間也是不能進行js的交互操做的。
可是一般能夠經過內嵌資源等方式來巧妙的進行讀寫訪問。Ajax通過特殊設置也能夠實現跨域Ajax通訊。不一樣源的框架間在必定條件限制下也能夠經過必定手段實現js交互。
CORS(Cross-Origin Resource Sharing,跨域資源共享)定義了在必須訪問跨域資源時,瀏覽器與服務器應該如何溝通。
CORS的基本思想是設置某些HTTP頭部字段讓瀏覽器和服務器進行溝通,從而決定請求或響應是應該成功仍是失敗。
請求和響應都 默認不包含cookie信息。
就是說跨源請求不提供憑據(包括cookie、HTTP認證及客戶端SSL證實等)。
若是跨源請求須要發送憑據,那麼解決辦法爲:
客戶端若使用XMLHttpRequst,那麼須要設置 xhr.withCredentials爲true;若使用Fetch,那麼須要設置請求參數 credentials爲'include'。
服務端設置響應頭 Access-Control-Allow-Credentials爲true
發送Ajax請求的頁面地址爲:http://localhost:3000/a;
請求目標的地址爲:http://sub.localhost:3001/b, 該地址提供一段json數據。
頁面a客戶端代碼:
<div>我是a</div> <button type="button" id="sendBtn">點我發送Ajax請求</button> <script> const sendBtn = document.getElementById('sendBtn'); sendBtn.addEventListener('click', function() { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { console.log(xhr.responseText); } } } xhr.open('get', '{{reqDest}}', true); xhr.send(null); });
頁面b的服務端重點代碼(by koa):
router.get('/b', ctx => { ctx.set('Access-Control-Allow-Origin','http://localhost:3000'); ctx.body = { 'name':'bonne', 'age':26 } });
現象:
在a頁面點擊按鈕能夠看到控制檯輸出了'{"name":"bonne","age":26}',即成功地獲取到了跨源數據資源。
將a頁面請求代碼作以下修改:
<div>我是a</div> <button type="button" id="sendBtn">點我發送Ajax請求</button> <script> const sendBtn = document.getElementById('sendBtn'); sendBtn.addEventListener('click', function() { fetch('{{reqDest}}', { mode: 'cors' }).then( res => { if (res.ok) { return res.json(); } else { throw new Error('Network response was not ok'); } }).then( resData => { console.log(resData); }).catch(err => { console.Error(err.message); }) }); </script>
其餘不變。
現象:
在a頁面點擊按鈕依然能夠看到控制檯輸出了'{"name":"bonne","age":26}',即便用該Fetch方式也成功地獲取到了跨源數據資源。
咱們都知道,img標籤能夠從任何網頁中加載圖像,不管是否跨域。圖像Ping就是利用了img標籤的這一功能。
圖像Ping是與服務器進行簡單、單向的跨域通訊的一種方式。數據能夠經過src地址的查詢字符串發送到服務器。瀏覽器能夠經過監聽load和error事件,判斷服務器是什麼時候接收到響應。
最經常使用於跟蹤用戶點擊頁面的行爲或廣告曝光次數。
例如咱們網站就是使用圖像Ping給廣告客戶的服務器發送圖像Ping來是的廣告客戶獲取廣告曝光次數的數據:
var track = new Image(); track.onload = function() { window.parent.ga('send', 'event', 'iPhone web app launch ad', 'Sent', imp, {'nonInteraction':1}); }; track.onerror = function() { window.parent.ga('send', 'event', 'iPhone web app launch ad', 'Fail', imp, {'nonInteraction':1}); }; track.src = imp;//imp爲廣告客戶的廣告曝光追蹤地址,實際上是一個白色小圓點圖片
script元素和img相似,都有能力不受限制地從其餘域加載資源。JSONP就是利用了script元素的這一功能。
JSONP是JSON with Padding(參數式JSON或填充式JSON),就是被包含在函數中調用的JSON。
JSONP由兩部分組成:數據 和 回調函數。 數據就是傳入回調函數中的JSON數據。
JSONP的工做過程:爲script標籤的src指定一個跨域的URL(即JSONP服務的地址),並在URL中指定回調函數名稱。由於JSONP服務最終返回的是有效的JavaScript代碼,請求完成後會當即執行咱們在url參數中指定的函數,而且會把咱們須要的json數據做爲參數傳入。因此,jsonp是須要服務器端進行相應的配合的。
發起jsonp請求的前端頁面相關代碼爲:
<script> function doSomething(jsonpData) { console.log(jsonpData); } </script> <script src="http://localhost:3000/?cb=doSomething"></script>
使用動態方式加載script亦可:
<script> function doSomething(jsonpData) { console.log(jsonpData); } var scriptElem = document.createElement('script'); script.src = 'http://localhost:3000/?cb=doSomething'; document.body.append(scriptElem); <script>
cb就是url中指定回調函數名稱的參數,一般是callback,這個是須要在服務端設置的。
展現該前端頁面的服務代碼:
const path = require('path'); const Koa = require('koa'); const Router = require('koa-router'); const logger = require('koa-logger'); const views = require('koa-views'); const app = new Koa(); const router = new Router(); app.use(logger()); app.use(views(path.resolve(__dirname,'views'))); async function showText(ctx) { await ctx.render('test'); } router.get('/', showText); app.use(router.routes()); app.listen(3001, () => { console.log('Listening 3001'); });
jsonp服務的代碼:
const Koa = require('koa'); const Router = require('koa-router'); const jsonp = require('koa-jsonp'); const logger = require('koa-logger'); const app = new Koa(); const router = new Router(); app.use(logger()); app.use(jsonp({ callbackName:'cb'//指定回調函數名稱的參數, defaults to 'callback' })); router.get('/', ctx => { ctx.body = { name:'Bonnie', age:26 } }); app.use(router.routes()); app.listen(3000, () => { console.log('Listening 3000'); });
頁面的端口爲3001,jsonp服務端口爲3000,造成跨域,可是頁面能夠完美獲取到jsonpData。
具體可參見我寫的用koa和中間件koa-jsonp實現jsonP服務的例子:
瀏覽器中不一樣域的框架之間是不能進行js的交互操做的。腳本試圖訪問的框架內容必須遵照同源策略。也就是說:
不一樣域的框架之間能夠進行js交互。
父頁面訪問子頁面:經過 contentWindow屬性,父頁面的腳本能夠訪問iframe元素所包含的子頁面的window對象。contentDocument屬性則引用了iframe中的文檔元素(等同於使用contentWindow.document),但IE8-不支持。
子頁面訪問父頁面:經過訪問 window.parent,腳本能夠從框架中引用它的父框架的window。
腳本沒法訪問非同源的window對象的幾乎全部屬性。
該同源策略即適於父窗體訪問子窗體的window對象,也適用於子窗體訪問父窗體的window對象。
頁面a的地址爲http://localhost:3000/a;
頁面b的地址爲http://localhost:3000/b;
頁面a中經過iframe嵌入頁面b。
頁面a代碼:
<div>我是a</div> <script> window.name = 'parentFrame'; window.globalvarA = 'aaa'; function onLoad() { const otherFrame = document.getElementById('otherFrame'); console.log('Parent console: Values of props from the child frame window:') console.log(`win:${otherFrame.contentWindow}`); console.log(`window.postMessage:${otherFrame.contentWindow.postMessage}`); console.log(`dom:${otherFrame.contentWindow.document}`); console.log(`name:${otherFrame.contentWindow.name}`); console.log(`globalvarB:${otherFrame.contentWindow.globalvarB}`); } </script> <iframe id="otherFrame" name="otherFrame" src='http://localhost:3000/b' onload="onLoad()"></iframe>
頁面b代碼:
<div>我是b</div> <script> window.globalvarB = 'bbb'; console.log('Child console:Values of props from the parent frame window:') console.log(`win:${window.parent}`); console.log(`win.postMessage:${window.parent.postMessage}`); console.log(`dom:${window.parent.document}`); console.log(`name:${window.parent.name}`); console.log(`globalvarA:${window.parent.globalvarA}`); </script>
在http://localhost:3000/a的瀏覽器窗口能夠看到a頁面正確載入了b頁面的內容。
在http://localhost:3000/a的控制檯能夠看到,a和b框架都有輸出,b框架輸出在a框架以前:
b框架輸出結果:
Child console:Values of props from the parent frame window: win:[object Window] win.postMessage:function () { [native code] } dom:[object HTMLDocument] name:parentFrame globalvarA:aaa
a框架輸出結果:
win:[object Window] window.postMessage:function () { [native code] } dom:[object HTMLDocument] name:otherFrame globalvarB:bbb
可見同源的父子框架之間js互相訪問window對象確實很是順暢。
頁面a的地址爲http://localhost:3000/a;
頁面b的地址爲http://sub.localhost:3001/b;
頁面a、b的嵌套關係不變,代碼也不變,除了a中iframe的src值修改成b的新地址http://sub.localhost:3001/b
頁面a代碼:
<div>我是a</div> <script> window.name = 'parentFrame'; window.globalvarA = 'aaa'; function onLoad() { const otherFrame = document.getElementById('otherFrame'); console.log('Parent console: Values of props from the child frame window:') try { console.log(`win:${otherFrame.contentWindow}`); } catch(err) { console.log('cannot get otherFrame.contentWindow'); } try { console.log(`window.postMessage:${otherFrame.contentWindow.postMessage}`); } catch(err) { console.log('cannot get otherFrame.contentWindow.postMessage'); } try { console.log(`dom:${otherFrame.contentWindow.document}`); } catch(err) { console.log('cannot get otherFrame.contentWindow.document'); } try { console.log(`name:${otherFrame.contentWindow.name}`); } catch(err) { console.log('cannot get otherFrame.contentWindow.name'); } try { console.log(`globalvarB:${otherFrame.contentWindow.globalvarB}`); } catch(err) { console.log('cannot get otherFrame.contentWindow.glovalvarB'); } } </script> <iframe id="otherFrame" name="otherFrame" src='http://sub.localhost:3001/b' onload="onLoad()"></iframe>
頁面b代碼:
<div>我是b</div> <script> window.globalvarB = 'bbb'; console.log('Child console:Values of props from the parent frame window:') try { console.log(`win:${window.parent}`); } catch(err) { console.log('cannot get window.parent'); } try { console.log(`window.postMessage:${window.parent.postMessage}`); } catch(err) { console.log('cannot get window.parent.postMessage'); } try { console.log(`dom:${window.parent.document}`); } catch(err) { console.log('cannot get window.parent.document'); } try { console.log(`name:${window.parent.name}`); } catch(err) { console.log('cannot get window.parent.name'); } try { console.log(`globalvarB:${window.parent.globalvarA}`); } catch(err) { console.log('cannot get window.parent.globalvarA'); } </script>
Tips1: koa中間件koa-subdomain能夠完成對子域名的劃分。
Tips2:使用try{} catch() {}能夠在報錯的時候不影響後續代碼執行,因此這裏把每一個window相關屬性的獲取都放在try{} catch(){}語句中
(1) 在http://localhost:3000/a的瀏覽器窗口, 能夠看到a頁面依然正確載入了b頁面的內容。
由此能夠再次說明,使用iframe載入html頁面自己是能夠跨域的(若是沒有對 'X-Frame-Options'響應頭進行限制)。
(2) 在http://localhost:3000/a控制檯,能夠看到:
b框架輸出:
Child console:Values of props from the parent frame window: cannot get window.parent window.postMessage:function () { [native code] } cannot get window.parent.document cannot get window.parent.name cannot get window.parent.globalvarA
框架a輸出:
Parent console: Values of props from the child frame window: cannot get otherFrame.contentWindow window.postMessage:function () { [native code] } cannot get otherFrame.contentWindow.document cannot get otherFrame.contentWindow.name cannot get otherFrame.contentWindow.glovalvarB
可見:
參見我寫的同源和跨源iframe交互操做的示例。
將頁面的 document.domain的值設置爲當前域或當前域的父域。詳見 1、中 2. 源的更改。
注意: 使用此方法實現跨域僅針對不一樣框架間js的交互有效,對於Ajax仍是無效。
在上述a.html和b.html中的script標籤中的第一行加上:
document.domain = 'localhost'
理論上在本地測試應該能夠成功了。但事實上會報錯:
Failed to set document.domain to localhost. 'localhost' is a top-level domain
實際上是由於localhost這個域名很特殊,這樣設置不合法。
解決辦法是經過修改C:\Windows\System32\drivers\etc\hosts文件,加上一行:
127.0.0.1 test.com
將localhost換成合法域名。
修改域名後,a能夠經過http://test.com:3000/a訪問。
可是,koa-subdomain中間件會失效,訪問b仍是隻有http://sub.localhost:3001/b,http://sub.test.com:3001/b並不會生效。
若是想要看到跨域結果,仍是去非本地的服務器上測試吧~~~
window有一個屬性name。window.name用於獲取或設置window的名稱。
window.name的特性:在一個window的生命週期內,該window載入的全部頁面都是共享一個window.name的。每一個被載入的頁面對該window.name都有讀寫的權限。若是新載入的頁面沒有對window.name進行重寫,那麼每個新載入的頁面均可以獲取到相同的window.name。
頁面a的地址爲http://localhost:3000/a;
頁面b的地址爲http://localhost:3000/b;
在頁面a中過5s將window.location改成頁面b的地址。
頁面a代碼:
<div>我是a</div> <script> window.name = '頁面a'; setTimeout(function() { window.location = 'http://localhost:3000/b'; }, 5000); </script>
頁面b代碼:
<div>我是b</div> <script> console.log(window.name); </script>
在http://localhost:3000/a能夠看到5s事後載入了頁面b,且控制檯輸出'頁面a'。即window.name並無由於載入新的頁面b而發生變化。
將a、b頁面的地址作以下修改:
頁面a的地址爲http://localhost:3000/a;
頁面b的地址爲http://sub.localhost:3001/b。
看到的結果和以前同樣。
因此,對於一個window, window.name不會由於window.location的改變而改變(除非新載入的頁面修改了這個值),不管新載入的頁面和以前的頁面是否存在跨域。
NOTE: window.name的值只能是 字符串的形式,這個字符串的大小最大能容許 2M左右甚至更大的一個容量,具體取決於不一樣的瀏覽器,但通常是夠用了。
若是上述a.html和b.html是跨域的,咱們知道a在經過修改window.location的方式載入b後,b依然能夠獲取以前a的window.name,但是這樣a頁面本身的內容已經丟失了。那麼如今假如b的window.name裏存儲有咱們須要的數據,如何在a頁面中得到來自b的window.name的數據呢?
咱們能夠在a中經過一個隱藏的iframe引入頁面b。iframe自己是能夠跨域的,因此這一點不用擔憂。然而咱們須要的是獲取這個iframe的name,由於跨域因此沒法進行js交互,因此天然也沒法獲取b的window.name。可是咱們能夠利用上述window.name的特性,在a中用js將該iframe的src修改成一個同源的頁面地址(假設爲c),這樣就能夠獲取c的window.name了。又由於這個iframe以前是b,只是從新又載入了c,因此c的window.name就是以前b的window.name,因此a就能夠經過獲取c的window.name獲取b的window.name了。
實踐代碼以下:
頁面a的地址爲http://localhost:3000/a;
頁面b的地址爲http://sub.localhost:3001/b;
頁面c的地址爲http://localhost:3000/c
頁面a代碼:
<div>我是a</div> <iframe style="display: none;" id="dataSource" src='http://sub.localhost:3001/b'></iframe> <script> const dataSourceIframe = document.getElementById('dataSource'); dataSourceIframe.onload = function() { dataSourceIframe.onload = function() { const data = dataSourceIframe.contentWindow.name; console.log(data); } dataSourceIframe.src = '/c'; } </script>
頁面b代碼:
<div>我是b</div> <script> window.name = JSON.stringify({ name:'Bonnie', age:26 }) </script>
在http://localhost:3000/a的控制檯打印出了數據 {"name":"Bonnie","age":26},實現了跨域。
tips: 每修改一次iframe的src都會觸發一次iframe的onload事件。
具體代碼參見我寫的經過window.name實現跨域的例子。
window.postMessage() 是html5引進的新方法,可使用它來向其它的window對象發送消息,不管這個window對象是屬於同源或不一樣源。
一般,對於兩個不一樣頁面的腳本,只有當執行它們的頁面位於具備相同的協議、端口號、主機時,這兩個腳本才能相互通訊。window.postMessage() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就能夠安全地實現跨源通訊。
發送數據的頁面調用postMessage方法:
otherWindow.postMessage(message, targetOrigin, [transfer]);
params:
otherWindow: 其餘窗口的一個引用,好比iframe的contentWindow屬性、執行window.open返回的窗口對象、或者是命名過或數值索引的window.frames。
message:將要發送到otherWindow的數據。能夠是string或object。
targetOrigin: 指定哪些window能接收到消息事件,即otherWindow的地址,其值能夠是字符串"*"(表示無限制)或者一個URI。 **若是你明確的知道消息應該發送到哪一個窗口,那麼請始終提供一個有確切值的targetOrigin,而不是*。不提供確切的目標將致使數據泄露到任何對數據感興趣的惡意站點。**
transfer (可選): 一串和message 同時傳遞的 Transferable 對象. 這些對象的全部權將被轉移給消息的接收方,而發送一方將再也不保有全部權。
window.postMessage() 方法被調用時,會在全部頁面腳本執行完畢以後向目標window派發一個 MessageEvent 消息,即觸發目標window的message事件。
接收數據的頁面監聽message事件。
message事件有的event對象有一些特殊的屬性:
event.data: 從發送數據的 window 中傳遞過來的對象。
event.origin: 調用 postMessage 時消息發送方窗口的 origin . 這個字符串由 協議、「://「、域名、「 : 端口號」拼接而成。例如 「https://example.org (隱含端口 443)」、「http://example.net (隱含端口 80)」、「http://example.com:8080」。請注意,這個origin不能保證是該窗口的當前或將來origin,由於postMessage被調用後可能被導航到不一樣的位置。
event.source:對發送數據的window對象的引用。使用它能夠在具備不一樣origin的兩個窗口之間創建雙向通訊。
頁面a的地址爲http://localhost:3000/a;
頁面b的地址爲http://sub.localhost:3001/b;
頁面a代碼:
<div>我是a</div> <script> const data = { name:'Bonnie', age:26 } function onLoad() { const otherFrame = document.getElementById('otherFrame'); otherFrame.contentWindow.postMessage(data,'http://sub.localhost:3001'); } </script> <iframe id="otherFrame" name="otherFrame" src='http://sub.localhost:3001/b' onload="onLoad()"></iframe>
頁面b代碼:
<div>我是b</div> <div id="messageResult"></div> <script> window.onmessage = function(e) { const messageResult = document.getElementById('messageResult'); messageResult.innerHTML = JSON.stringify(e.data); console.log(e.data); console.log(e.origin); console.log(e.source); } </script>
在http://localhost:3000/a的瀏覽器窗口能夠看到加載的頁面b中的messageresult部分輸出了正確的數據。
在http://localhost:3000/a的控制檯,能夠看到b框架的輸出:
{name: "Bonnie", age: 26} http://localhost:3000 global {window: global, self: global, location: Location, closed: false, frames: global, …}
即 e.origin是 http://localhost:3000 , e.source是global {window: global, self: global, location: Location, closed: false, frames: global, …}
具體可參見我寫的使用postMessage實現跨域的例子。
Web Sockets是一種基於ws協議的技術。使用它能夠在客戶端和服務器之間創建一個單獨的、持久的、全雙工的、雙向的通訊。
在JavaScript中建立了Web Socket以後,會有一個HTTP請求發送到瀏覽器以發起鏈接。在取得服務器響應後,創建的鏈接會使用HTTP升級從HTTP協議換爲Web Socket協議。 也就是說,標準的HTTP服務器沒法實現Web Sockets,只有支持Web Socket協議的服務器才能實現
因爲Web Sockets使用了 自定義協議,因此其URL的模式也有一些不一樣。未加密的鏈接是 ws://,而非http:// ; 加密的鏈接是 wss://, 而非https://。
而HTTP的特色是:
var socket=new WebSocket("ws://www.example.com/server.php"); socket.send("Hello world!"); socket.onmessage=function(event){ var data=event.data; //處理數據,能夠用這些數據更新頁面的某部分 }
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/X-Frame-Options
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe
https://www.jianshu.com/p/b587dd1b7086
https://mp.weixin.qq.com/s/asmzA8a1HuYQxyx8K0q-9g?
https://www.techwalla.com/articles/how-to-change-your-local-host-name
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
《JavaScript高級程序設計》21.5