一、同源策略
同源策略(Same origin policy)是由 NetScape 公司在1995年提出的一种安全策略;同源策略只是一个约定,提出后被各个浏览器厂商采纳,并以各自的方式实现了同源策略。
同源策略:是浏览器最核心也最基本的安全功能,会约束浏览器的行为;
同源策略会限制浏览器:只允许本域内的脚本读写本域内的资源,不允许访问本域外的资源。
判断是否同源?
判断要素有三:协议、域名、端口号;
(三者全部一致才视为同源,即属于同一个域;否则视为非同源。)
限制范围
无法共享 cookie, localStorage, indexDB。
无法操作彼此的 dom 元素。
无法发送 ajax 请求。
二、跨域
域
域(Domain)是Windows网络中独立运行的单位;
域之间相互访问则需要建立信任关系,信任关系是连接在域与域之间的桥梁;
当一个域与其他域建立了信任关系后,2个域之间不但可以按需要相互进行管理,还可以跨网分配文件和打印机等设备资源,使不同的域之间实现网络资源的共享与管理.
跨域:突破同源策略的限制,在两个不同的域之间(非同源页面)实现资源交互。
跨域分类:
1)使用 Ajax 引发的跨域问题
当调用 Ajax 时:调用 Ajax 发送请求的页面 所在的域,和被请求页面所在的域不一致
2)类似页面嵌入 ifream 引起的跨域问题
当操作 ifream 内引入的元素时:ifream 所属页面的域,和 ifream 引入页面的域不一致
三、跨域实现方法?
1. JSONP:
JSONP 就是跨域的一种实现方式。(JSONP 全称 JSON with Padding,是数据格式 JSON 的一种 “使用模式”,可用于解决主流浏览器的跨域数据访问的问题。)
JSONP 是一种非正式传输协议,该协议的一个要点就是允许用户传递一个 callback 参数给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
JSONP 的原理:
利用 script 标签不受同源策略影响,可以跨域引入外部资源的特性,让服务器端返回可执行的 JS 函数,将要返回的数据作为参数传进函数,以此实现跨域加载数据的目的
此时,绕过 Ajax,并未使用它,但同样达成了请求数据的目的
[](【备注】——script 标签引用资源得本质是:
1)向 src 发送请求
2)将资源下载到当前页面
3)当资源加载完毕后,把该资源当做 JS 代码来立刻执行——【备注】)
JSONP 的使用:
1、动态创建 script 标签,src 地址指向数据接口,并传递 callback 参数
2、定义数据处理函数
3、服务端接收请求,解析参数,计算数,返回回调函数字符串
4、将回调函数字符串引入页面并作为 JS 去执行:此时会调用数据处理函数,数据会作为数据处理函数的参数被处理计算出一个结果
JSONP 的优缺点
优点
1)因 script 隶属于 HTML 的标签,所以不存在兼容问题
缺点
1)因需使用 URL 引入资源,所以 JSONP 仅支持 get 请求
2)因 script 标签会将资源作为 JS 代码执行,所以可能会被注入恶意代码
2、CORS
CORS 全称 Cross-Origin Resource Sharing,即:跨来源资源共享。它是一份浏览器技术的规范,提供了Web服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,是JSONP模式的现代版
CORS 的原理:
1、当使用 XMLHttpRequest 发送请求时,如果浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin;
2、后台进行一系列处理,如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin;
3、浏览器判断该响应头中是否包含 Origin 的值:
4、如果包含浏览器则会处理响应,前端就可以拿到响应数据;
5、如果不包含浏览器直接驳回,此时前端无法拿到响应数据。
(简单来说,就是浏览器匹配请求头和响应头,如果符合要求便可拿到数据,否则无法拿到数据。整个过程都是由浏览器自动完成。这就像一个白名单,代表着谁可以拿到数据。)
CORS 的使用:
前端:正常使用 AJAX 发送请求
服务端:若确定接受请求,则在返回结果中加入响应头:Access-Control-Allow-Origin
CORS 的优缺点:
优点
1)使用简单方便、更为安全
2)支持 POST 请求方式
缺点
1)CORS 是一种新型跨域问题的解决方案:存在兼容问题——仅支持 IE10 以上
3、降域
降域仍是解决跨域问题的一种方案,通过双向设置 document.domain 的值,解决主域名下的跨域问题
降域的原理
比如,有两个二级域名:a.yang.com 和 b.yang.com,可通过设定 document.domain 的值为主域名:yang.com 的方式,突破浏览器的同源策略限制,来获取和操作对方的元素
这就好比,小 A 和小 B,手里拿着城主的令牌,通过哨卡时才能畅行无阻,否则哨卡不让过
这也就决定了,降域具有很大的局限性,适用范围较小,适合在同一主域名下使用;需要有降的空间,方可使用降域方式
降域的使用
A 页面域为:a.yang.com
B 页面域为:b.yang.com
A 和 B 两页面都需加入该行代码:document.domain = 'yang.com'; ,‘yang.com 是 a.yang.com 和 b.yang.com 的主域名
4、postMessage
那么当两个主域名完全不同时,应该如何处理呢?来看看新方法 postMessage。
postMessage 原理
postMessage 是 HTML5 中新增方法,可实现跨域通信;
postMessage 并不是向服务器读写资源,只是向外发送消息而已;可以把它当做使用手机发送短信消息,仅此而已。
也就是:A 页面向 B 页面发送了一条消息,B 页面会接受到该消息,如果 B 页面需要该消息,则监听 message;否则无需关心该消息
。
postMessage 的使用
发送方:为目标元素添加事件处理程序,监听事件类型
接收方:为 window 添加事件处理程序,事件类型为 messag
postMessage 的注意点
这样就解除了降域的限制,可以将 postMessage 应用到各个不同的域之间,实现跨域访问。该方式比较安全,因为对方并不能直接操控我方资源,仅仅是发了一条消息,相当于指令,而最终操作权限仍在自己手中
虽然解决了降域的限制,但是:postMessage 是 window 的一个方法,话句话说:它的弱点是只能向 window 窗口发送消息。所以使用时至少要有一个 window 才行,postMessage 不可以向其他域名发送消息
(注意)
最后:降域和 postMessage 都是小众的降域方式,并不是经常使用,若有需求可按需选择
5、nginx代理跨域(服务器代理)
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
6、 WebSocket协议跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
1.)前端代码:
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
Nodejs socket后台:
var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
7、location.hash + iframe跨域
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
1.)a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
2.)b.html:(http://www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
3.)c.html:(http://www.domain1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
8、window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
1.)a.html:(http://www.domain1.com/a.html)
var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});
2.)proxy.html:(http://www.domain1.com/proxy....
中间代理页,与a.html同域,内容为空即可。
3.)b.html:(http://www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。