最近的项目中,在前端项目中访问另一个前端页面,同时还有数据的交互,在使用iframe中总是提示跨域请求,在解决问题中,查看了很多资料,同时了解了一下前端跨域的原因,以及常见的解决方案,进行总结如下,防止今后再次遇到。
一、跨域
我们定义JS跨域是指通过JS在不同的域中进行相互通信或者数据传输。这里的域一般是指协议、域名(或主机地址)、端口,只要有其中一个不同,都会被当作是不同的域。而这个域是通过浏览器的同源策略进行限制的。
二、浏览器的同源策略
同源策略/SOP(Same Origin Policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
在比较URL不同时,我们先来查看一下URL的组成部分,一般URL(统一资源定位符)分为:
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
scheme为具体的协议,包括http、https、ssh、ftp、pop3等,每个协议一般对应不同的参数默认部分。这里就讲解一下http、https相关的问题。
常见的URL组成为: 协议+域名(ip)+端口+路径
如果协议,端口和域名对于两个页面是相同的,则两个页面具有相同的源。
比如,以http://blog.anumbrella.net/example/index.html
这个URL来说,协议是http,域名为blog.anumbrella.net
,端口为80(默认端口)。
它的同源情况如下:
URL | 结果 | 原因 |
同源 | 只有路径不同 | |
同源 | 只有路径不同 | |
不同源 | 不同协议(http和https) | |
不同源 | 不同端口(80和81) | |
不同源 | 不同域名(dev和blog) |
限制范围
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS对象无法获得
- AJAX 请求不能发送
目的
同源策略保障了用户信息的安全,防止了恶意的网站窃取数据。
比如这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
需求
既然有安全问题,那为什么又要跨域呢? 这是因为有时公司内部有多个不同的子域,比如一个是location.company.com
, 而应用是放在app.company.com
, 这时想从app.company.com
去访问location.company.com
的资源就属于跨域。
跨源网络访问
同源策略控制了不同源之间的交互,以下的实例是允许跨域资源嵌入的:
- script标签允许跨域嵌入脚本,稍后介绍的JSONP就是利用这个“漏洞”来实现。
- img标签、link标签、@font-face不受跨域影响。
- video和audio嵌入的资源。
- iframe载入的任何资源。(不是iframe之间的通信)
-
<object>
、<embed>
和<applet>
的插件。 - WebSocket不受同源策略的限制。
三、常见的解决方案
1、通过JSONP跨域
JSONP是JSON with Padding(填充式JSON)的简写,是应用JSON的一种新方法,只不过是被包含在函数调用中的JSON。
它通过借用script标签不受同源限制的这个特性,通过动态的给页面添加一个script标签,利用事先声明好的数据处理函数来获取数据。
在JSONP中包含两部分:回调函数和数据。其中,回调函数是当响应到来时要放在当前页面被调用的函数。而数据,就是传入回调函数中的JSON字符串,也就是回调函数的参数了。
(1)、原生实现
function handleResponse(response) {
console.log(response.data);
}
var script = document.createElement("script");
script.src = "http://example.com/jsonp/getSomething?uid=123&callback=hadleResponse"
document.body.insertBefore(script, document.body.firstChild);
/*handleResponse({"data": "hey"})*/
它的过程是这样子的:
- 当我们通过新建一个script标签请求时,后台会根据相应的参数来生成相应的JSON数据。比如说上面这个链接,传递了handleResponse给后台,然后后台根据这个参数再结合数据生成了handleResponse({“data”: “hey”})。
- 紧接着,这个返回的JSON数据其实就可以被当成一个js脚本,就是对一个函数的调用。
- 由于我们事先已经声明了这么一个回调函数,于是当资源加载进来的时候,直接就对函数进行调用,于是数据当然就能获取到了。
至此,跨域通信完成。
(2)、在JQuery中使用JSONP
在JQuery中的AJAX中,已经封装了JSONP,下面简单介绍一下如何去使用。
$.ajax({
url: 'http://dev.anumbrella.net/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "handleCallback", // 自定义回调函数名
success: function (data) {
console.log(data);
},
error: function (data) {
console.log(data);
}
});
在AJAX中,主要设置dataType类型为jsonp
。对于jsonp
参数来说,默认值是callback,而jsonpCallback参数的值默认是JQuery自己生成的。如果想自己指定一个回调函数,可像代码中对jsonpCallback进行设置。上面的代码中,最终的URL将会是http://dev.anumbrella.net/login?callback=handleCallback
。
JSONP的优缺点
JSONP的优点是:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。
JSONP的缺点则是:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
2、通过修改document.domain+iframe来跨子域
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
(1)、父窗口:(http://parent.anumbrella.net/a.html)
<iframe id="iframe" src="http://child.anumbrella.net/b.html"></iframe>
<script>
document.domain = 'anumbrella.net';
var user = 'admin';
</script>
(2)、父窗口:(http://child.anumbrella.net/b.html)
<script>
document.domain = 'anumbrella.net';
// 获取父窗口中变量
console.log('get js data from parent ---> ' + window.parent.user);
</script>
3、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.example1.com/a.html)
<iframe id="iframe" src="http://www.example2.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) {
console.log('data from c.html ---> ' + res);
}
</script>
(2)、b.html:(http://www.example2.com/b.html)
<iframe id="iframe" src="http://www.example1.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.example1.com/c.html)
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
4、window.name + iframe跨域
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
(1)、a.html:(http://www.example1.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.example1.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.example2.com/b.html', function(data){
console.log(data);
});
(2)、proxy.html:(http://www.example1.com/proxy.html)
中间代理页,这是一个在www.example1.com域名下的空页面。
(3)、b.html:(http://www.example2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。
5、window.postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。
父窗口和子窗口都可以通过message事件,监听对方的消息。
父窗口和子窗口都可以通过message事件,监听对方的消息。
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
(1)、a.html:(http://www.example1.com/a.html)
<iframe id="iframe" src="http://www.example2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向example1传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.example1.com');
};
// 接受example2返回数据
window.addEventListener('message', function(e) {
console.log('data from example2 ---> ' + e.data);
}, false);
</script>
(2)、b.html:(http://www.example2.com/b.html)
<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from example1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.example1.com');
}
}, false);
</script>
message事件的事件对象event,提供以下三个属性。
- event.source:发送消息的窗口
- event.origin: 消息发向的网址
- event.data: 消息内容
6、Nginx代理跨域
(1)、Nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}
(2)、Nginx反向代理接口跨域
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过Nginx配置一个代理服务器(域名与example1相同,端口不同)做跳板机,反向代理访问example2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
Nginx的配置如下:
#proxy服务器
server {
listen 81;
server_name www.example1.com;
location / {
proxy_pass http://www.example2.com:8080; #反向代理
proxy_cookie_domain www.dexample1.com www.example2.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.example1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
7、 WebSocket协议跨域
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端代码:
<div>input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.example2.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>
更多的使用WebSocket知识,也可以查看我原来写的文档——RabbitMQ学习(八)——做WebSocket消息代理,集成Spring Boot实现消息实时推送。
8、 使用CORS允许跨源访问
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。
在CORS请求中分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
简单请求:如果不用带cookie,只需服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
CORS与JSONP的使用目的相同,但是比JSONP更强大。