跨域
是指一个域下面的文档或者脚本视图去请求另一个域下的资源
- 资源跳转:A链接,重定向,表单提交
- 资源嵌入:
<link>
,<script>
,<img>
,<frame>
等dom
标签, 还有样式中的background: url(), @font-face() 等文件外链 - 脚本请求:
js
发起的ajax
请求,dom
和js
对象的跨域请求
同源策略
由NetScape公司1995年引入浏览器,是浏览器最核心也最基本的安全功能。如果没有这个功能,浏览器很容易受到XSS,CSFR等攻击。同源即 协议 + 域名 + 端口 三者相同,即使两个域名指向同一个ip地址,也不是同源。
同源策略限制一下几种行为
-
Cookie
,LocalStorage
和IndexDB
无法读取 -
DOM
和JS
对象无法获得 -
AJAX
请求不能发送
跨域解决方案
-
通过
JSONP
跨域JSONP 只能get请求
<script src="http://www.b.com/request?callback=jsonPCallBack"> function jsonPCallBack(content) {...} </script>
$.ajax("http://www.b.com/request", { jsonpCallback: "moty", dataType: "jsonp", success: function(json) {...} });
-
document.domain + iframe 跨域
a. 这两个域名必须属于同一个一级域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域。
b. 如果是同一级域名下的子域名,如:m.a.com, api.a.com, 需要在
javascript
里面设置domain
才行document.domain = 'a.com';
news.baidu.com下的news.html页面:
<script> document.domain = 'baidu.com'; var ifr = document.createElement('iframe'); ifr.src = 'map.baidu.com/map.html'; ifr.style.display = 'none'; document.body.appendChild(ifr); ifr.onload = function(){ var doc = ifr.contentDocument || ifr.contentWindow.document; // 这里可以操作map.baidu.com下的map.html页面 var oUl = doc.getElementById('ul1'); alert(oUl.innerHTML); ifr.onload = null; }; </script>
map.baidu.com下的map.html页面:
<ul id="ul1">我是map.baidu.com中的ul</ul> <script> document.domain = 'baidu.com'; </script>
-
location.hash + iframe
原理是利用location.hash来进行传值。
在url: http://a.com#helloword中的‘#helloworld’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。
假设域名a.com下的文件cs1.html要和cnblogs.com域名下的cs2.html传递信息,cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向cnblogs.com域名下的cs2.html页面,这时的hash值可以做参数传递用。
cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。
同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一点有变化则获取获取hash值。
a.html下的文件cs1.html
function startRequest(){ var ifr = document.createElement('iframe'); ifr.style.display = 'none'; ifr.src = 'http://www.cnblogs.com/lab/cscript/cs2.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);
cnblogs.com下的cs2.html
//模拟一个简单的参数处理操作 switch(location.hash){ case '#paramdo': callBack(); break; case '#paramset': //do something…… break; } function callBack(){ try { parent.location.hash = 'somedata'; } catch (e) { // ie、chrome的安全机制无法修改parent.location.hash, // 所以要利用一个中间的cnblogs域下的代理iframe var ifrproxy = document.createElement('iframe'); ifrproxy.style.display = 'none'; // 注意该文件在"a.com"域下 ifrproxy.src = 'http://a.com/test/cscript/cs3.html#somedata'; document.body.appendChild(ifrproxy); } }
a.html下的文件cs3.html
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1);
-
window.name + iframe
window.name (一般在js代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,这里要注意的是每个iframe都有包裹它的window,而这个window是top window的子窗口,而它自然也有window.name的属性, window.name属性的神奇之处在于name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。
window.name = 'abc'; window.name; // abc window.location = 'http://www.baidu.com'; window.name; // abc
a.html, proxy.html 同属于一个域下,b.html是另一个域的
b.html内容
<script> window.name = 'b.html\'s data'; </script>
a.html
<script type="text/javascript"> var otherLoaded = false, iframe = document.createElement('iframe'), loadfn = function() { if (otherLoaded) { var data = iframe.contentWindow.name; // 读取数据 alert(data); // 弹出b.html's data // 清理工作 iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } else if (!otherLoaded) { otherLoaded = true; // 设置的代理文件 iframe.contentWindow.location = "http://localhost:8001/proxy.html"; } }; iframe.src = 'http://localhost:8002/b.html'; if (iframe.attachEvent) { iframe.attachEvent('onload', loadfn); } else { iframe.onload = loadfn; } document.body.appendChild(iframe); </script>
可以看到, 第一次设置iframe的地址为b.html, 这样的话b.html会被加载进来,但是并不能直接访问iframe.contentWindow.name, 因为a.html和b.html目前不同源,如果将loadfn的实现改为
var data = iframe.contentWindow.name;
,会出来这个错误:a.html: Uncaught DOMException: Blocked a frame with origin "http://localhost:8001" from accessing a cross-origin frame.
那怎么办呢, 既然不同源, 就改成同源呗, 所以将iframe地址改成与a.html同源的proxy.html,由于window.name在地址变化时值不变, 所以iframe.contentWindow.name的值还是之前的值, 也就是b.html窗口的值, 而又满足的同源的要求, 所以可以访问成功。
-
postMessage
window.postMessage
的功能是允许程序员跨域在两个窗口/frames间发送数据信息。基本上,它就像是跨域的AJAX,但不是浏览器跟服务器之间交互,而是在两个客户端之间通信。安全着想,接受消息的时候应该检验来源,也就是event.origin或者event.source。a.html
<h1 class="header">page A</h1> <div class="mb20"> <textarea name="ta" id="data" cols="30" rows="5">hello world</textarea> <button style="font-size:20px;" onclick="send()">post message</button> </div> <iframe src="http://localhost:9022/b.html" id="child"
style="display: block; border: 1px dashed #ccc; height: 300px;"></iframe>
<script>
function send() {
var data = document.querySelector('#data').value;
// 触发跨域子页面的messag事件
window.frames[0].postMessage(data, 'http://localhost:9022/');
}
window.addEventListener('message', function(messageEvent) {
if (messageEvent.origin !== 'http://localhost:9022/') return
var data = messageEvent.data;
console.info('message from child:', data);
}, false);
</script>
b.html
```html
<h1 class="header">page B</h1>
<input type="text" id="inp" value="some contents..">
<button onclick="send()">send</button>
<script>
window.addEventListener('message', function(ev) {
if (ev.source !== window.parent) {return;}
var data = ev.data;
console.info('message from parent:', data);
}, false);
function send() {
var data = document.querySelector('#inp').value;
// 若父页面的域名和指定的不一致,则postMessage失败
window.parent.postMessage(data, 'http://localhost:9011/');
// 触发父页面的message事件
// parent.postMessage(data, '*');
}
</script>
-
跨域资源共享(CORS)
CORS 通过新增一系列的HTTP头,让服务器能声明那些来源能访问该服务器上的资源,GET以外的请求,会以OPTIONS请求方式发一个预请求,从而得知服务器对资源请求支持的HTTP方法,在确认服务器允许跨域请求资源的情况下,以实际的HTTP请求方法发送真正的请求。
-
请求头:
Origin
:普通的HTTP请求也会带有,在CORS中专门作为Origin信息供后端比对,表明来源域。
Access-Control-Request-Method
:接下来请求的方法,例如PUT, DELETE等等
Access-Control-Request-Headers
:自定义的头部,所有用setRequestHeader方法设置的头部都将会以逗号隔开的形式包含在这个头中
-
http响应头
然后浏览器再根据服务器的返回值判断是否发送非简单请求。简单请求前面讲过是直接发送,只是多加一个origin字段表明跨域请求的来源。然后服务器处理完请求之后,会再返回结果中加上如下控制字段
Access-Control-Allow-Origin
:允许跨域访问的域,可以是一个域的列表,也可以是通配符"*"。这里要注意Origin规则只对域名有效,并不会对子目录有效。即http://foo.example/subdir/ 是无效的。但是不同子域名需要分开设置,这里的规则可以参照同源策略
Access-Control-Allow-Credentials
:是否允许请求带有验证信息,XMLHttpRequest请求的withCredentials标志设置为true时,认证通过,浏览器才将数据给脚本程序。
Access-Control-Expose-Headers
:允许脚本访问的返回头,请求成功后,脚本可以在XMLHttpRequest中访问这些头的信息
Access-Control-Max-Age
:缓存此次请求的秒数。在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据,非常有用,大幅优化请求次数
Access-Control-Allow-Methods
:允许使用的请求方法,以逗号隔开
Access-Control-Allow-Headers
:自定义的头部,以逗号隔开,大小写不敏感
-
nginx代理跨域
nodejs中间件代理跨域
websocket协议跨域