浏览器的同源策略限制了 从一个源加载的文档或脚本与来自另外一个源的资源的交互。它是隔离潜在恶意文档的关键安全机制。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